サイトのトップへ戻る

Google App Engine ドキュメント日本語訳

データストアを使用する

スケーラブルウェブアプリケーションにおいて、データの保存というのは難しい問題です。 ある時点においてユーザーは数あるウェブサーバの中の一つとやり取りをし、次にユーザーが送信したリクエストは前のリクエストとは違うサーバに送られることがあります。 ウェブサーバは、多くのマシン(おそらく世界中の異なる場所にある)にまたがっているデータの影響を受けます。

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構造体を作成します:

hello.go
View on GitHub
type Greeting struct {
        Author  string
        Content string
        Date    time.Time
}

これで挨拶文のためのデータタイプが準備できたので、アプリケーションは新しいGreeting 値を作成してデータストアに格納できます。 新しく追加する sign ハンドラをその処理を行います:

hello.go
View on GitHub
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 ハンドラはデータストアに格納された挨拶文を参照します:

hello.go
View on GitHub
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個までです。

hello.go
View on GitHub
q := datastore.NewQuery("Greeting").Ancestor(guestbookKey(c)).Order("-Date").Limit(10)

それから q.GetAll(c, &greetings)を呼び出します。 q.GetAll(c, &greetings)ではクエリを実行してその結果を greetings sliceに追加します。

hello.go
View on GitHub
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 デベロップメントサーバのデータストアをクリアする

デベロップメントウェブサーバでは、アプリケーションをテストする際にはローカル版のデータストアを使用します。 データストアでは一時ファイルが使用されています。 データは一時ファイルが存在する限り保持され、あなたリセットをしない限りウェブサーバこれらのファイルをリセットしません。

起動する前にデベロップメントサーバのデータストアを消去したい場合は、Go デベロップメントサーバリファレンスを参照してください。デベロップメントサーバのデータストア設定オプションが説明されています。



データストアを使用したサンプルの完全版

以下は、データストアに挨拶文を保存する新しいバージョンの myapp/hello.goです。 このページの以降では新規追加した部分について説明します。

hello.go
View on GitHub
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 データストアのインデックス設定を参照してください。



A Word About Datastore Indexes

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ファイルでのクエリーの定義は以下のようになります:

index.yaml
View on GitHub
indexes:
- kind: Greeting
  ancestor: yes
  properties:
  - name: Date
    direction: desc

データストアインデックスに関しては、全て データストアインデックスのページで読めます。 index.yaml ファイルの正しい記述方法については Go データストアのインデックス設定で読めます。



次は…

これでゲストブックアプリケーションでは、Google アカウントを使ってユーザーを認証し、ユーザーがメッセージを送信し、他のユーザーが残したメッセージを表示することができるようになりました。 App Engineが自動的にスケーリング処理を行うため、人が増えてもアプリケーションを再構築する必要はありません。

アプリケーションをアップロードする >>