スケーラブルウェブアプリケーションにおいて、データの保存というのは難しい問題です。 ある時点においてユーザーは数あるウェブサーバの中の一つとやり取りをし、次にユーザーが送信したリクエストは前のリクエストとは違うサーバに送られることがあります。 ウェブサーバは、多くのマシン(おそらく世界中の異なる場所にある)にまたがっているデータの影響を受けます。
Google App Engineのおかげで、それらを心配する必要はありません。 簡単なAPIを使えば裏で App Engineのインフラが、配布や複製やデータの負荷分散といった全てのことを行ってくれます —そして強力なクエリとトランザクションも同様に使用できます。
App Engineのデータリポジトリである High Replication Datastore (HRD)はPaxos アルゴリズムを使用して 複数のデータセンタにまたがってデータを複製します。 データは、エンティティとして知られるオブジェクト内の、データストアに書き込まれます。 各エンティティは自身を唯一に識別するためのキーを持っています。 各エンティティは、必要に応じて親として指定できる別のエンティティを持っています; 持っている方のエンティティは、親エンティティの子になります。 こうしてデータストア内のエンティティはファイルシステムのディレクトリ構造に似た階層構造空間を形成しています。 エンティティの親、親の親、などはそれぞれのアンセスターと呼びます。 エンティティの子、子の子、などはそれぞれのデセンダントと呼びます。 親のないエンティティは ルートエンティティです。
データストアは致命的な障害に対しては非常に耐性がありますが、データの一貫性保証に関してはあなたがこれまでに馴染んだものと異なっています。 エンティティが共通のアンセスターの子になるということは、同じエンティティグループに属するということです; 共通するアンセスターの持つキーをグループの親キーと呼び、エンティティグループを識別するのに使われます。 一つのエンティティグループに対するクエリーはアンセスタークエリーと呼ばれ、特定のエンティティキーではなく親キーを参照します。 エンティティグループは、一貫性とトランザクションの両方における最小単位になります: whereas queries over multiple entity groups may return stale, eventually consistent results, those limited to a single entity group always return up-to-date, strongly consistent results.
T このガイドのコードサンプルでは関連するエンティティをエンティティグループに組み入れ、 一貫性の強い結果を返すためにこれらのエンティティグループにアンセスタークエリを使用しています。 サンプルコードのコメントでは、 we highlight some ways this might affect the design of your application. 詳細情報についてはStructuring Data for Strong Consistencyを参照してください。
ゲストブックアプリケーションの場合、ユーザーが投稿した挨拶文を保存したいですね。 各挨拶文には書いた人の名前、メッセージ文、メッセージが投稿された日時が含まれ、時系列順に表示することができます。:
このデータを表すために、Greeting
という名前のGo構造体を作成します:
type Greeting struct { Author string Content string Date time.Time }
これで挨拶文のためのデータタイプが準備できたので、アプリケーションは新しいGreeting
値を作成してデータストアに格納できます。
新しく追加する sign
ハンドラをその処理を行います:
func sign(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) g := Greeting{ Content: r.FormValue("content"), Date: time.Now(), } if u := user.Current(c); u != nil { g.Author = u.String() } // We set the same parent key on every Greeting entity to ensure each Greeting // is in the same entity group. Queries across the single entity group // will be consistent. However, the write rate to a single entity group // should be limited to ~1/second. key := datastore.NewIncompleteKey(c, "Greeting", guestbookKey(c)) _, err := datastore.Put(c, key, &g) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusFound) }
上記では Greeting
値を新規に作成し、そのAuthor
フィールドに現在のユーザーを、Content
フィールドにそのユーザーが投稿したデータを、
Date
フィールドに現在の時間を設定します。
最後に、 datastore.Put
で新しい値をデータストアに保存します。
We pass it a new, incomplete key so that the datastore will create a new
key for this record automatically.
High Replication Datastore上でのクエリはエンティティグループ内でのみ強い一貫性があるので、 この例では各挨拶文に同じ親を設定することで一つのゲストブックの全ての挨拶文を同一エンティティグループに割り当てています。 これにより、ユーザーには書き込んだ直後に常に挨拶文が表示されます。 しかし、同一のエンティティグループに書き込むことができる速度は、1秒ごとに1回に制限されています。 実際のアプリケーションを設計する時はこれを覚えておく必要があります。 Memcacheのようなサービスを使用することで、 エンティティグループをまたがった書き込みをする場合に書き込み直後に最新の結果が表示されないといった事態を軽減できます。
datastore.Query
で取得する
datastore
パッケージで、データストアを参照したり結果を反復処理したりするQuery
を使用できます。
新しく追加する root
ハンドラはデータストアに格納された挨拶文を参照します:
func root(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) // Ancestor queries, as shown here, are strongly consistent with the High // Replication Datastore. Queries that span entity groups are eventually // consistent. If we omitted the .Ancestor from this query there would be // a slight chance that Greeting that had just been written would not // show up in a query. q := datastore.NewQuery("Greeting").Ancestor(guestbookKey(c)).Order("-Date").Limit(10) greetings := make([]Greeting, 0, 10) if _, err := q.GetAll(c, &greetings); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := guestbookTemplate.Execute(w, greetings); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }
最初の関数では、Greeting
オブジェクトをリクエストするためのQuery
値を作成しています。
対象のGreeting
オブジェクトは、ルートゲストブックキーのデセンダントで、日付の降順に並べられ、最大10個までです。
q := datastore.NewQuery("Greeting").Ancestor(guestbookKey(c)).Order("-Date").Limit(10)
それから q.GetAll(c, &greetings)
を呼び出します。
q.GetAll(c, &greetings)
ではクエリを実行してその結果を greetings
sliceに追加します。
greetings := make([]Greeting, 0, 10) if _, err := q.GetAll(c, &greetings); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
最後に、guestbookTemplate.Execute
関数でこれら挨拶文を含むHTMLを描写し、http.ResponseWriter
へ書き出します。
テンプレート言語の詳細については、text/template package ドキュメントを参照してください。
ここでは text/template
をラップしたパッケージであるhtml/templateを使用しているので、
HTML テンプレート内のコンテンツは自動的にエスケープ処理され、スクリプトのクラスがインジェクション攻撃を受けるのを防ぎます。
Datastore APIの完全な説明については、 Datastore リファレンスを参照してください。
デベロップメントウェブサーバでは、アプリケーションをテストする際にはローカル版のデータストアを使用します。 データストアでは一時ファイルが使用されています。 データは一時ファイルが存在する限り保持され、あなたリセットをしない限りウェブサーバこれらのファイルをリセットしません。
起動する前にデベロップメントサーバのデータストアを消去したい場合は、Go デベロップメントサーバリファレンスを参照してください。デベロップメントサーバのデータストア設定オプションが説明されています。
以下は、データストアに挨拶文を保存する新しいバージョンの myapp/hello.go
です。
このページの以降では新規追加した部分について説明します。
package guestbook import ( "html/template" "net/http" "time" "appengine" "appengine/datastore" "appengine/user" ) type Greeting struct { Author string Content string Date time.Time } func init() { http.HandleFunc("/", root) http.HandleFunc("/sign", sign) } // guestbookKey returns the key used for all guestbook entries. func guestbookKey(c appengine.Context) *datastore.Key { // The string "default_guestbook" here could be varied to have multiple guestbooks. return datastore.NewKey(c, "Guestbook", "default_guestbook", 0, nil) } func root(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) // Ancestor queries, as shown here, are strongly consistent with the High // Replication Datastore. Queries that span entity groups are eventually // consistent. If we omitted the .Ancestor from this query there would be // a slight chance that Greeting that had just been written would not // show up in a query. q := datastore.NewQuery("Greeting").Ancestor(guestbookKey(c)).Order("-Date").Limit(10) greetings := make([]Greeting, 0, 10) if _, err := q.GetAll(c, &greetings); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := guestbookTemplate.Execute(w, greetings); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } var guestbookTemplate = template.Must(template.New("book").Parse(` <html> <head> <title>Go Guestbook</title> </head> <body> {{range .}} {{with .Author}} <p><b>{{.}}</b> wrote:</p> {{else}} <p>An anonymous person wrote:</p> {{end}} <pre>{{.Content}}</pre> {{end}} <form action="/sign" method="post"> <div><textarea name="content" rows="3" cols="60"></textarea></div> <div><input type="submit" value="Sign Guestbook"></div> </form> </body> </html> `)) func sign(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) g := Greeting{ Content: r.FormValue("content"), Date: time.Now(), } if u := user.Current(c); u != nil { g.Author = u.String() } // We set the same parent key on every Greeting entity to ensure each Greeting // is in the same entity group. Queries across the single entity group // will be consistent. However, the write rate to a single entity group // should be limited to ~1/second. key := datastore.NewIncompleteKey(c, "Greeting", guestbookKey(c)) _, err := datastore.Put(c, key, &g) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusFound) }
myapp/hello.go
を上記の内容に書き換え、ブラウザ上でhttp://localhost:8080/を再読み込みしてください。
いくつかメッセージを投稿し、メッセージの保存と表示が正しく行われているか確認してください。
警告!
ローカル環境のアプリケーションでクエリを実行すると、index.yaml
の作成や更新が行われます。
index.yaml
が無かったり欠落していた場合、必要なインデックスが設定されていないので、
アップロードしたアプリケーションを実行した時にインデックスエラーが表示されます。
正式版でのインデックスエラーを見逃さないようにするため、アプリケーションをアップロードする前には常に、ローカル環境で少なくとも一回は新規クエリをテストしてください。
詳細情報についてはGo データストアのインデックス設定を参照してください。
Every query in the App Engine Datastore is computed from one or more indexes—tables that map ordered property values to entity keys. This is how App Engine is able to serve results quickly regardless of the size of your application's Datastore. Many queries can be computed from the builtin indexes, but for queries that are more complex the Datastore requires a custom index. Without a custom index, the Datastore can't execute these queries efficiently.
For example, our guestbook application above filters by guestbook entries and orders by
date, using an ancestor query and a sort order. This requires a custom index to be
specified in your application's index.yaml
file. You can edit this file
manually or, as noted in the warning box earlier on this page, you can take care of it
automatically by running the queries in your application locally. Once the
index is defined in index.yaml
, uploading your application will also
upload your custom index information.
index.yaml
ファイルでのクエリーの定義は以下のようになります:
indexes: - kind: Greeting ancestor: yes properties: - name: Date direction: desc
データストアインデックスに関しては、全て データストアインデックスのページで読めます。
index.yaml
ファイルの正しい記述方法については Go データストアのインデックス設定で読めます。
これでゲストブックアプリケーションでは、Google アカウントを使ってユーザーを認証し、ユーザーがメッセージを送信し、他のユーザーが残したメッセージを表示することができるようになりました。 App Engineが自動的にスケーリング処理を行うため、人が増えてもアプリケーションを再構築する必要はありません。