以前のバンドル サービス用の App Engine Datastore API

このドキュメントでは、Datastore に格納されるオブジェクトのデータモデル、API を使用したクエリの作成方法、トランザクションの処理方法について説明します。datastore パッケージの内容については、datastore パッケージ リファレンスをご覧ください。

エンティティ

Datastore のオブジェクトをエンティティと呼びます。エンティティは 1 つ以上の名前付きプロパティを持ち、各プロパティが 1 つ以上の値を持ちます。プロパティ値には、整数、浮動小数点数、文字列、日付、バイナリデータなど、さまざまなデータ型があります。複数の値を持つプロパティに対してクエリを実行すると、いずれかの値がクエリの条件を満たしているかどうかが評価されます。このため、メンバーかどうかを評価するときに、こうしたプロパティを活用できます。

種類、キー、識別子

Datastore の各エンティティは特定の種類に属しています。種類とは、クエリに対応できるようにエンティティを分類するものです。たとえば、人事アプリケーションでは、社内の各従業員が Employee という種類のエンティティに相当します。また、各エンティティに独自のキーがあり、これによりエンティティが一意に識別されます。キーは次のコンポーネントで構成されています。

  • エンティティの種類
  • 識別子。次のいずれかになります。
    • キー名の文字列
    • 整数の ID
  • オプションの祖先パス。Datastore 階層内のエンティティの位置を指定します。

識別子は、エンティティの作成時に割り当てられます。識別子はエンティティのキーの一部であるため、エンティティに永続的に関連付けられており、変更できません。識別子を割り当てる方法は次の 2 通りあります。

  • アプリケーションでエンティティの固有のキー名文字列を指定する。
  • エンティティに整数の数値 ID が自動的に割り当てられるように Datastore を設定する。

識別子の割り当て

ランタイム サーバーは、次の 2 種類の自動 ID ポリシーを使用して ID を自動生成するように構成できます。

  • default ポリシーは、おおむね均一に分散された一連の ID を生成します。各 ID の長さは最大で 16 桁です。
  • legacy ポリシーは、連続しない小さい整数の ID を作成します。

エンティティの ID をユーザーに表示する場合や、ID の順序に従ってなんらかの処理を行う場合は、手動で割り振ることをおすすめします。

祖先パス

Cloud Datastore 内の各エンティティは、ファイル システムのディレクトリ構造と同様の階層的に構造化された空間を形成します。エンティティを作成するときに、別のエンティティを親として設定することもできます。新しいエンティティは、この親エンティティの子になります(ファイル システムとは異なり、親エンティティが実際に存在している必要はありません)。親を持たないエンティティは「ルート エンティティ」となります。エンティティと親との割り当ては永続的であり、エンティティの作成後は変更できません。同じ親を持つ 2 つのエンティティ、または 2 つのルート エンティティ(親を持たないエンティティ)に同じ数値 ID が割り当てられることはありません。

エンティティの親、親の親、さらにその親などは順に「祖先」として位置付けられ、エンティティの子、子の子、さらにその子などは順に「子孫」として位置付けられます。ルート エンティティとその子孫のエンティティは、すべて同じエンティティ グループに属します。ルート エンティティから始まり、親から子を経由して対象のエンティティに至るまでのエンティティの連なりのことを、エンティティの「祖先パス」といいます。エンティティを識別する完全なキーは、エンティティの祖先パスから始まってそのエンティティ自身で終わる一連の「種類と識別子のペア」で構成されています。

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

ルート エンティティの場合は祖先パスが空で、エンティティ自身の種類と識別子だけでキーが構成されています。

[Person:GreatGrandpa]

このコンセプトを次の図に示します。

エンティティ グループ内のルート エンティティと子エンティティの関係を示す図

クエリとインデックス

アプリケーションは、キーによって Cloud Datastore からエンティティを直接取得するだけでなく、クエリを実行して、エンティティのプロパティ値によってエンティティを取得することもできます。 クエリは特定の種類のエンティティに対して動作します。クエリでは、エンティティのプロパティ値、キー、祖先に対してフィルタを指定でき、任意の数のエンティティを結果として返す(または返さない)ことができます。また、並べ替え順を指定し、プロパティ値を基準にして結果を並べ替えることもできます。結果には、フィルタと並べ替え順で指定されているすべてのプロパティに対応する 1 つ以上の値を持つエンティティの中で、指定されているすべてのフィルタ条件をプロパティ値が満たしているものがすべて含まれます。クエリはエンティティ全体や射影されたエンティティを返すこともできれば、エンティティのキーだけを返すこともできます。

一般的に、クエリは次のような要素で構成されます。

クエリを実行すると、指定した種類のすべてのエンティティの中で、指定したすべてのフィルタを満たすものが取得され、指定した順序で並べ替えられます。クエリは読み取り専用として実行されます。

注: メモリを節約し、パフォーマンスを向上させるため、クエリでは可能な限り返される結果の数に対する制限を指定してください。

クエリに祖先フィルタを設定すると、指定した祖先の下位にあるエンティティ グループだけに結果を絞り込むことができます。このようなクエリは祖先クエリと呼ばれます。祖先クエリのデフォルトでは、強整合性を持つ結果が返されます。つまり、データに対する直近の変更を反映した最新の結果が保証されます。これに対して、祖先クエリでないクエリは、単一のエンティティ グループだけでなく Datastore 全体を対象としますが、結果整合性しか持たないため、最新でない結果を返す可能性があります。強整合性が必要なアプリケーションでデータ構造を定義する場合は、祖先を指定してクエリで取得できるように、関連するエンティティを同じエンティティ グループに配置することも検討してください。

App Engine は、エンティティの各プロパティに単純なインデックスを事前定義します。さらに、App Engine アプリケーションでは、index.yaml という名前のインデックス構成ファイルでカスタム インデックスを定義できます。開発用サーバーは、既存のインデックスでは実行できないクエリが出現したときに、このファイルに自動的に候補を追加します。アプリケーションをアップロードする前にこのファイルを編集して、インデックスを手動で調整できます。

注: インデックス ベースのクエリ メカニズムはさまざまなクエリをサポートしているため、ほとんどのアプリケーションに適しています。ただし、他のデータベース テクノロジーで一般にサポートされているいくつかの種類のクエリがサポートされていません。具体的には、結合クエリおよび集計クエリは Datastore のクエリエンジン内でサポートされていません。Datastore クエリの制限については、Datastore クエリのページをご覧ください。

トランザクション

エンティティの挿入、更新、削除はすべてトランザクションのコンテキスト内で試行されます。1 つのトランザクションで、このようなオペレーションが複数実行される場合もあります。トランザクションでは、データの整合性を維持するため、すべてのオペレーションを 1 つの単位として Datastore に適用します。いずれかのオペレーションが失敗した場合、すべてのオペレーションが適用されません。

1 つのエンティティに対する複数のオペレーションを 1 つのトランザクションで実行できます。たとえば、オブジェクトのカウンタ フィールドの値を増やす場合、カウンタの値を読み取り、新しい値を計算してフィールドに保存する必要があります。トランザクションを使用しないでこのオペレーションを行うと、値の読み取り時点から更新時点までの間にカウンタの値が別のプロセスによって更新されてしまうことがあり、アプリケーションがその更新された値を上書きすることになる可能性があります。1 つのトランザクションで読み取り、計算、書き込みを行うことで、他のプロセスによる影響を防ぐことができます。

トランザクションとエンティティ グループ

トランザクションで使用できるクエリは祖先クエリだけです。つまり、トランザクションで実行するクエリの対象は単一のエンティティ グループに限定されます。トランザクション自体は、複数のエンティティに適用できます。このエンティティは、単一のエンティティ グループに属するものに限らず、最大 25 の複数のエンティティ グループ(クロスグループ トランザクションの場合)に属する場合もあります。

Datastore では、オプティミスティック同時実行を使用してトランザクションを管理します。2 つ以上のトランザクションが、既存エンティティの更新や新しいエンティティの作成といった変更を同じエンティティ グループに対して同時に実行しようとすると、最初に commit したトランザクションが成功し、残りのトランザクションは commit に失敗します。とはいえ、失敗した残りのトランザクションを使って、更新されたデータに対してオペレーションを再試行できます。このような特長があるため、特定のエンティティ グループのエンティティに対して同時に可能な書き込み数は制限されます。

クロスグループ トランザクション

異なるエンティティ グループに属する複数のエンティティに対するトランザクションをクロスグループ(XG)トランザクションといいます。1 つのトランザクションは、最大 25 のエンティティ グループに適用できます。あるトランザクションの適用先となるいずれかのエンティティ グループが他の同時実行トランザクションによって変更される場合を除き、そのトランザクションの処理は成功します。アトミックな書き込みを行うためだけの理由で同じ祖先の下位に異なる種類のデータを無理に配置する必要がないため、データを柔軟に編成できます。

単一グループ トランザクションと同様に、XG トランザクションでは祖先クエリ以外のクエリを実行することはできません。とはいえ、複数の祖先クエリがある場合、各クエリを別個のエンティティ グループに対して実行できます。非トランザクション クエリ(祖先クエリ以外のクエリ)では、その前に commit されたトランザクションの結果のすべてまたは一部を取得できる場合もありますし、できない場合もあります(この問題の背景については、Datastore への書き込みとデータの可視性をご覧ください)。ただし、そのような非トランザクション クエリでは、部分的に commit された単一グループ トランザクションの結果より、部分的に commit された XG トランザクションの結果を返す可能性が高くなります。

1 つのエンティティ グループしか処理しない XG トランザクションのパフォーマンスとコストは、XG 以外の単一グループ トランザクションとまったく同じになります。複数のエンティティ グループを処理する XG トランザクションの場合、オペレーションのコストは XG 以外のトランザクションで実行した場合と同じになりますが、レイテンシが長くなる可能性があります。

Datastore への書き込みとデータの可視性

データは、以下の 2 つのフェーズで Datastore に書き込まれます。

  1. commit フェーズ。エンティティ データは大半のレプリカのトランザクション ログに記録されます。エンティティ データが記録されなかったレプリカは、最新のログがないことを示すマークが付きます。
  2. 適用フェーズ。レプリカごとに個別に発生します。このフェーズでは、次の 2 つのアクションが並列処理されます。
    • エンティティ データがそのレプリカに書き込まれます。
    • エンティティのインデックス行がそのレプリカに書き込まれます(この処理は、データ自体の書き込みより時間がかかる場合があります)。

書き込みオペレーションは commit フェーズの直後に戻り、適用フェーズが非同期で実行されます。このフェーズは、レプリカごとに異なる時間に実行される可能性が高いため、commit フェーズの完了から数百ミリ秒ほどの遅延が発生する場合があります。commit フェーズでエラーが発生すると、自動的に再試行されますが、エラーが繰り返し発生すると、Datastore がアプリケーション例外を通知するエラー メッセージを返します。commit フェーズが完了した後、特定のレプリカで適用フェーズが失敗した場合、以下のいずれかが発生すると、そのレプリカで適用が完了までロール フォワードされます。

  • Datastore の定期的なスイープで未完了の commit ジョブの有無が検査され、それらが適用される。
  • 影響を受けるエンティティ グループを使用する特定のオペレーション(getputdelete、祖先クエリ)が行われたために、次のオペレーションに進む前に、commit 後に未適用の変更が、オペレーションを実行中のレプリカで完了する。

この書き込み動作は、commit フェーズと適用フェーズのそれぞれで、アプリケーションでデータの取得がどのように可能になるかとそのタイミングに影響を及ぼす可能性があります。

  • 書き込みオペレーションでタイムアウト エラーが発生した場合、データの読み取りを行わないと、オペレーションが成功したのか失敗したのか判断できません。
  • Datastore の get と祖先クエリを実行すると、実行対象となるレプリカに対して未処理のすべての変更が適用されるため、それまでに成功したすべてのトランザクションは一貫した状態で取得できるようになります。つまり、get オペレーション(更新されたエンティティをそのキーでルックアップする)を行うと、そのエンティティの最新バージョンが必ず表示されます。
  • 祖先クエリ以外のクエリは、最新のトランザクションが適用されていないレプリカで実行される可能性があるため、古い結果が返される場合があります。たとえ未処理のトランザクションを必ず適用するようなオペレーションを実行したとしても、そのようになる可能性があります。クエリが前のオペレーションとは異なるレプリカで実行される場合があるためです。
  • 同時変更のタイミングが祖先クエリ以外のクエリの結果に影響を及ぼす可能性があります。クエリ条件を満たしていたエンティティが変更され、条件を満たさなくなった場合、クエリを実行したレプリカのインデックスに変更がまだ適用されていないと、エンティティがクエリの結果セットに残ってしまう可能性があります。

Datastore 統計情報

データストアは、アプリケーション用に保存されているデータに関する統計情報を維持します。たとえば特定の種類のエンティティ数や特定の型のプロパティ値が使用している容量などです。この統計情報は、Google Cloud コンソールの Datastore ダッシュボード ページで確認できます。Datastore API を使用して、特別な名前のエンティティをクエリすることで、アプリケーション内からこの値にプログラムでアクセスすることもできます。詳細については、Go 1.11 のデータストア統計をご覧ください。

Go 1.11 Datastore の例

Go では、Datastore のエンティティは struct 値から作成されます。この構造体のフィールドが、エンティティのプロパティになります。新しいエンティティを作成するには、保存する値を設定し、キーを作成して、その両方を datastore.Put() に渡します。既存のエンティティを更新するには、同じキーを使用して別の Put() を実行します。Datastore からエンティティを取得するには、まずエンティティのマーシャリング解除先となる値を設定し、次に、キーとその値へのポインタを datastore.Get() に渡します。

Datastore でデータの保存と取得を行う例は、次に示すとおりです。

import (
	"fmt"
	"net/http"
	"time"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/user"
)

type Employee struct {
	Name     string
	Role     string
	HireDate time.Time
	Account  string
}

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

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

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

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

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

詳細については、Datastore リファレンスをご覧ください。