全てのデータストアクエリはインデックスを使用します。インデックスとはクエリの結果を指定された順番に並べたものを含んだテーブルのことです。App Engine アプリケーションは datastore-indexes.xml という名前の設定ファイルにインデックスを定義します。開発サーバーは、対応するインデックスが未設定のクエリが実行されたときに、設定ファイルに自動的に候補を追加することができます。
インデックスに基づいたクエリのメカニズムは多くの一般的な種類のクエリをサポートしますが、他のデータベース技術で慣れ親しんでいるクエリの中にはサポートされないものもあります。
クエリの概要クエリは条件のセットに一致するエンティティをデータストアから取得します。クエリはエンティティの kind 、エンティティのプロパティ値に基づく0個以上の条件("フィルタ"と呼ばれたりします)、そして0個以上のソート順を指定します。クエリを実行すると指定された kind の、指定された全ての条件を満たすエンティティを全て取得し、指定された順に並び替えます。
クエリは、エンティティ自体を返す代わりに、キーだけを返すこともできます。
クエリのフィルタフィルタ にはフィールド名、演算子、値を指定します。値は、他のプロパティを参照したり他のプロパティを使用して計算したりせずに、アプリケーションで指定しなければなりません。使用できる演算子は
"equal", "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual", "isNotNll", "startsWith", "notEqual", "in" です。結果として得られるエンティティは全てのフィルタに一致しなければなりません。フィルタの論理結合は "and" です。"notEqual" 演算子は実際には2つのクエリを実行します。他のフィルタは全て同じままで、
"notEqual" は1つだけで、また他の不等号フィルタを含むこともできません (訳注 「次の不等式フィルタの議論」についてはかかれていないみたい)。"in" 演算子も同様に複数のクエリを実行します。他のフィルタは同じままで、
です。結果はリスト内の項目の順番にマージされます。クエリに2つ以上の
"in" フィルタがある場合、各リスト項目の組み合わせの数だけのクエリが実行されます。"notEqual" や "in" を含むクエリのサブクエリの上限は30個までです。Date hireDate = ...;EmployeeMeta e = EmployeeMeta.get();List<Employee> list = Datastore.query(e) .filter(e.lastName.equal("Smith"), e.hireDate.greaterThan(hireDate)) .asList();属性が文字列の場合 startsWith 演算子を使用することができます。
EmployeeMeta e = EmployeeMeta.get();List<Employee> list = Datastore.query(e) .filter(e.name.startsWith("A")) .asList();クエリのソート順ソート順 にはプロパティと昇順・降順を指定します。結果は指定された順序でソートされて返されます。
EmployeeMeta e = EmployeeMeta.get();List<Employee> list = Datastore.query(e) .sort(e.hireDate.asc, e.firstName.desc) .asList();クエリのオフセットとリミットクエリにはアプリケーションに返される結果のオフセットとリミットを指定することができます。オフセットは完全な結果セットの中でどの結果を先頭にして返すかを、リミットは結果の数を、それぞれ指定します。例えば、オフセットが5でリミットが10の場合、6番目から15番目の結果が返されます。
開始オフセットはパフォーマンスに影響を与えます。データストアは全ての結果を取得した後で、開始オフセットよりも前の結果を破棄しなければならないからです。例えば、オフセットが5でリミットが10の場合、15個の結果をデータストアから取得した後に、最初の5個を破棄してから残りの10個をアプリケーションに返します。
List<Employee> list = Datastore.query(Employee.class)
.offset(5) .limit(10) .asList();キーだけを取得するクエリクエリは次のようにしてキーだけを返すことができます。
List<Key> keys = Datastore.query(Employee.class)
.filter(...) .sort(...) .asKeyList();単一の結果を取得するクエリクエリは単一の結果(一致するエンティティがなければnull)を返すことができます。結果が複数だった場合は com.google.appengine.api.datastor.PreparedQuery.TooManyResultsException 例外が発生します。
Employee emp = Datastore.query(Employee.class)
.filter(...) .asSingle();Query Model Iteratorクエリはモデルをイテレーターとして返すことができます。 Iterator<Employee> emps = Datastore.query(Employee.class) .filter(...) .asIterator();Query Key Iteratorクエリはキーをイテレーターとして返すことができます。 Iterator<Key> keys = Datastore.query(Employee.class).asKeyIterator();祖先クエリ次のように祖先を指定してクエリを実行することができます。
Key ancestorKey = ...;List<Child> list = Datastore.query(Child.class, ancestorKey).asList();kind なしの祖先クエリkind を指定せずに祖先クエリを実行することができます。kind なしの祖先クエリにフィルタやソート順を指定することはできません(キーの昇順がデフォルトです)。
Key ancestorKey = ...;List<Entity> list = Datastore.query(ancestorKey).asList();インデックスの概要App Engine データストアはアプリケーションが使用する全てのクエリに対してインデックスを保持します。アプリケーションがデータストアのエンティティに変更を加えたら、データストアは正しい結果になるようにインデックスを更新します。アプリケーションがクエリを実行した際にはデータストアは対応するインデックスから直接結果をフェッチします。
アプリケーションはクエリで使用される kind、フィルタプロパティと演算子、ソート順をそれぞれ組み合わせたインデックスを持ちます。
クエリの例を考えてみましょう。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.lastName.equal("Smith"), p.height.lessThan(72)) .sort(p.height.desc);このクエリに対するインデックスは Person kind エンティティ用のキーのテーブルで、height プロパティの値と lastName プロパティの値を持つ列があります。このインデックスは height の降順に並べられます。
同じ形式で違うフィルタ値のクエリは同じインデックスを使います。例えば、次のクエリは上記のクエリと同じインデックスを使います。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.lastName.equal("Jones"), p.height.lessThan(64)) .sort(p.height.desc);データストアは次のステップでクエリを実行します。
インデックステーブルには、フィルタやソート順で使用されている全てのプロパティに対する列があります。行は次の側面によって並び替えられます。
これにより、このインデックスを使って実行することが可能な全てのクエリの結果は、テーブル内の連続した行に置かれます。
この仕組みは広い範囲のクエリをサポートし多くのアプリケーションに適しています。しかし、他のデータベース技術で慣れ親しんでいるクエリの中にはサポートされないものもあります。
フィルタで使用されているプロパティを持っていないエンティティはクエリでは取得できないインデックスによって参照される全てのプロパティを持つエンティティだけがインデックスに含まれます。エンティティがインデックスに参照されるプロパティを持っていなければ、エンティティはインデックスには現れません。クエリはインデックスを使用するのでクエリの結果にそのエンティティが含まれることはありません。
App Engine データストアはプロパティを持たないエンティティとプロパティに null 値を持つエンティティを区別することに注意してください。
インデックスづけされないプロパティインデックスづけできないプロパティはクエリで見つけることができません。これには、Text 型や Blob 型の値を持つプロパティと同様に、インデックスづけしないようにマークされたプロパティも含まれます。
プロパティをインデックスづけしないように宣言するには @Attribute(unindexed = true) アノテーションをフィールドの定義につけます。
@Attribute(unindexed = true)private String unindexedString;あるプロパティに対するフィルタやソート順を持つクエリは、そのプロパティの値がText 型や Blob 型、またはインデックスづけしないとマークされているエンティティには決して一致しません。クエリのフィルタやソート順に対して、そのような値を持つプロパティはまるでプロパティ自体が設定されていないかのように振舞います。
混在した型のプロパティ値は型で並び替えられる2つのエンティティに同じ名前で別の値型のプロパティがある場合、そのプロパティのインデックスは、エンティティをまず値型で並び替え、次にその型の中で適切な順番に並び替えます。例えば、2つのエンティティに「age」という名前のプロパティがあって、一方は整数値、もう一方は文字列値である場合に「age」プロパティで並び替えると、値自体には関係なく整数値のエンティティが常に文字列値のエンティティよりも前に現れます。
このことは整数値と浮動小数点値の場合に特に重要です。データストアはこの二つの値型を別の型として扱います。全ての整数値は浮動小数点値よりも前に並べられるため、38という整数値のプロパティは37.5という浮動小数点値のプロパティよりも前に並べられます。
インデックスの定義と設定App Engine はデフォルトで、いくつかの単純なクエリのためのインデックスを作成します。その他のクエリ用には、アプリケーションが datastore-indexes.xml という名前の設定ファイルにインデックスを指定しなければなりません。対応するインデックスがないクエリを App Engine 上で実行しようとしたらクエリは失敗します。
App Engine は次のような形式のクエリに対するインデックスを自動生成します。
次のような他の形式のクエリは datastore-indexes.xml にインデックスの指定が必要です。
開発サーバーを使用するとインデックスの設定が簡単になります。というのも、開発サーバーは対応するインデックスがないクエリが実行された場合に、クエリの実行を失敗させるのではなく、クエリを実行できるようにインデックスの設定を生成することができるからです。アプリケーションで実行されうる全てのクエリ(kind、祖先、フィルタ、ソート順の全ての組み合わせ)をローカルテストで呼び出すのであれば、生成された登録情報で完全なインデックスのセットが表されます。そうでない場合には、アプリケーションをアップロードする前にインデックスを見直して調整することもできます。
アプリケーションの WAR の WEB-INF/ ディレクトリにある datastore-indexes.xml という名前の設定ファイルを使用して、インデックスを手動で定義することができます。自動インデックス設定(以下で説明します)が有効になっている場合、開発サーバーは WEB-INF/appengine-generated/ に datastore-indexes-auto.xml という名前のファイルにインデックスの設定を生成して、両方のファイルをもってインデックスのフルセットとします。
次のクエリの例をもう一度考えてみましょう。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.lastName.equal("Smith"), p.height.lessThan(72)) .sort(p.height.desc);このクエリに必要なインデックスの定義は、 datastore-indexes.xml に次のように指定されます。
<?xml version="1.0" encoding="utf-8"?><datastore-indexes xmlns="http://appengine.google.com/ns/datastore-indexes/1.0" autoGenerate="true"> <datastore-index kind="Person" ancestor="false"> <property name="lastName" direction="asc" /> <property name="height" direction="desc" /> </datastore-index></datastore-indexes>datastore-indexes.xml の <datastore-indexes> 要素が上記のように autoGenerate="true" となっているか、datastore-indexes.xml ファイルが存在しない場合、自動インデックス設定が有効となります。自動インデックス設定が有効だと、開発サーバーでこのクエリが実行されたときにインデックス設定が存在しなければ datastore-indexes-auto.xml にインデックス設定が追加されます。
datastore-indexes.xml と datastore-indexes-auto.xml の詳細については Java データストアのインデックスの設定 を参照してください。
クエリに対する制限インデックスを使用するクエリのメカニズムの性質のため、クエリができることには少し制限があります。
あるプロパティをフィルタまたはソートするには、そのプロパティが存在する必要があるあるプロパティに対するクエリのフィルタ条件やソート順が設定されている場合、エンティティがそのプロパティに値を持っている(null も可)こともまた条件になります。指定されたプロパティを持たないエンティティに対してクエリを実行することはできません。
(データストアはスキーマレスなので)データストアのエンティティは同じ kind の別のエンティティが持っているプロパティを持つ必要はありません。フィルタやソート順で使用されているプロパティを持っていないエンティティは、そのクエリ用のインデックスを作成する際に省略されます。
不等式フィルタは1つのプロパティだけにしか使えない
クエリは、クエリの持つ全てのフィルタの中で、1つのプロパティに対してだけ不等式フィルタ (<, <=, >=, >) を使用することができます。
例えば次のクエリは可能です。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.birthYear.greaterThanOrEqual(minBirthYearParam), p.birthYear.lessThanOrEqual(maxBirthYearParam));しかし、次のクエリはダメです。同じクエリの中で2つのプロパティに対して不等式フィルタを使用しているからです。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.birthYear.greaterThanOrEqual(minBirthYearParam), p.height.greaterThanOrEqual(minHeightParam));不等式フィルタで使用されているプロパティは他のソート順よりも先にソートする必要があるクエリに不等式フィルタと1つ以上のソート順がある場合、クエリは不等式フィルタで使用されているプロパティに対するソート順を持ち、そのソート順は他のプロパティに対するソート準よりも前になければなりません。
次のクエリは有効ではありません。不等式フィルタで使用されているプロパティがソート順にないからです。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.birthYear.greaterThanOrEqual(minBirthYearParam)) .sort(p.lastName.asc);同様に、次のクエリも有効ではありません。フィルタのプロパティに対するソート順が、他のプロパティのソート順の前にないからです。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.birthYear.greaterThanOrEqual(minBirthYearParam)) .sort(p.lastName.asc, p.birthYear.asc);次のクエリは有効です。
PersonMeta p = PersonMeta.get();Datastore.query(p) .filter(p.birthYear.greaterThanOrEqual(minBirthYearParam)) .sort(p.birthYear.asc, p.lastName.asc);不等式フィルタに一致する全ての結果を取得するために、クエリはインデックステーブルで最初に一致する行から一致しなくなる行までの連続する結果を返します。連続する行が完全な結果セットを表すようにするために、行は他のソート順よりも先に不等式フィルタによって並び替えられている必要があります。
複数値のプロパティとソート順複数値プロパティは、そのインデックスづけの方法のために、ソート順が通常とは異なります。
[1, 9]は昇順でも降順でも[4, 5, 6, 7]の前に来るという結果になり、通常とは異なります。
複数値プロパティに等式フィルタが設定されている場合、ソート順は無視される十分に注意してください。複数値プロパティに対して等式フィルタとソート順の両方が指定されているクエリでは、ソート順が無視されます。これは単値プロパティにとっての単純な最適化です。単値プロパティの場合は全ての結果がそのプロパティに同じ値を持っているので、並び替える必要がないのです。
しかし、複数値プロパティはそれ以外の値を持つ場合があります。ソート順が無視されるので、クエリの結果はソート順が適用された場合とは異なる並び順になっている可能性があります。
インメモリのフィルタとソートSlim3 Datastore はインメモリのフィルタとソートをサポートします。インメモリのフィルタとソートはデータストアのクエリ結果に適合するようになっています。
BlogMeta b = BlogMeta.get();List<Blog> list = Datastore.query(b). .filter(b.contrent.startsWith("AAA")) .filterInMemory(b.title.endsWith("BBB")) .sortInMemory(b.createdAt.desc) .asList();データストアのクエリには「不等式フィルタは1つのプロパティだけにしか使えない」といった制約がいくつかありますが、インメモリのフィルタとソートを使えばそのような制約を乗り越えることができます。
クエリフィルタでは "contains" や "endsWith" はサポートされていませんが、インメモリフィルタではこれらがサポートされています。
大きなエンティティとインデックス爆発全てのエンティティの全てのプロパティ(Text や Blob 以外)は、少なくとも1つのインデックステーブルに追加されます。デフォルトで作成されるシングルプロパティインデックスや、datastore-indexes.xml に記述されたインデックスの中でそのプロパティを参照する全てのインデックスに追加されるのです。エンティティの各単値プロパティは、シングルプロパティインデックスに一度保存され、そのプロパティが参照されているそれぞれのカスタムインデックスに一度ずつ保存されます*2。これらのインデックス項目はプロパティ値が変更されるたびに更新される必要があるので、そのプロパティを参照するインデックスが多ければ多いほど、プロパティの更新には時間がかかるようになります。
エンティティの更新にかかる時間があまり長くならないように、データストアは1つのエンティティが持つことのできるインデックス項目の数を制限しています。この制限は大きい(5000)のでほとんどのアプリケーションには関係ありません。しかし、制限にかかる場合もあります。例えば、非常に沢山の単値プロパティを持つエンティティはインデックス項目の制限値を超える可能性があります。
複数値プロパティはそれぞれの値を別々の項目としてインデックスに保存します。エンティティにプロパティが1つしかなくても、そのプロパティが非常に多くの値を持つ場合は、インデックス項目の制限値を超える可能性があります。
例えば次のインデックス( datastore-indexes.xml に記述されたもの) は MyModel という kind のエンティティの x と y というプロパティを持ちます。
<?xml version="1.0" encoding="utf-8"?><datastore-indexes> <datastore-index kind="MyModel"> <property name="x" direction="asc" /> <property name="y" direction="asc" /> </datastore-index></datastore-indexes>次のコードは、x と y にそれぞれ2つずつの値を設定してエンティティを作成します。
MyModel m = new MyModel();m.setX(Arrays.asList("one", "two"));m.setY(Arrays.asList("three", "four"));Datastore.put(m);これらの値を正確に表すと、インデックスは12のプロパティ値を保存しなければなりません。12のプロパティとは、自動生成のインデックスに x と y それぞれ2つずつ、カスタムインデックスに x と y の4つの順列それぞれに2つずつのことです。複数値プロパティに沢山の値を持っている場合、1つのプロパティに対して非常に多くのインデックス項目を保存しなければならないことになります。複数値プロパティを複数参照するインデックスは「インデックス爆発」と呼ばれることがあります。値の数が少なくても非常に大きなものとなることがあるからです。
Datastore.put() の結果がインデックス項目数の上限を超える場合、java.lang.IllegalArgumentException(Too many indexed properties for entity) という例外が発生して呼び出しは失敗します。エンティティの制限を超えてしまうようなインデックスを新たに作成すると、インデックスに対するクエリは失敗しインデックスは管理コンソールで "Error" 状態として表示されます。
リストプロパティを使うカスタムインデックスが必要なクエリを使わなければ、インデックス爆発を避けることができます。上記の通り、これには複数の並び替え順、等式と不等式が混ざったフィルタ、祖先フィルタを持つクエリが含まれます。 クエリカーソルクエリカーソルを使用するとアプリケーションはクエリを実行して一連の結果を取得し、その次のウェブリクエストで同じクエリに対して追加の結果をフェッチすることができます。 クエリオフセットのオーバーヘッドはありません。アプリケーションはあるクエリの結果をフェッチした後に、最後にフェッチされた結果の後の結果セットの位置を表すエンコード文字列(「カーソル」)を要求することができます。アプリケーションはカーソルを使用して、最後の位置から始まる追加の結果をフェッチすることができます。 カーソルとはフェッチ処理後の次のクエリ開始位置を表す base64 エンコード文字列です。アプリケーションはカーソルをデータストアやメモリキャッシュ、タスクキューのタスクペイロードに保存することができます。将来のリクエストハンドラーは同じクエリを実行することができ、カーソルによって表される位置からの結果を返し始めるようデータストアに教えるために、そのクエリにカーソルを含めることができます。カーソルは元々のクエリを実行したアプリケーションだけが使用可能で、同じクエリを続けるためだけに使用することができます。 Tip: ウェブフォーム内などでデータストアカーソルをクライアントに渡したり、クライアントからカーソル値を受け取ったりすることは一般的に安全です。クライアントはカーソル値を変更して元々のクエリ以外の結果にアクセスすることはできません。しかし base64 エンコードされた値をデコードすれば、結果エンティティの情報を暴くことができます。結果エンティティの情報とは、キー(アプリケーションID、kind、キー名またはID、全ての祖先キー)やクエリに使用されたプロパティ(フィルタとソート順に含まれるプロパティ)等の情報です。ユーザーにそのような情報を知られたくなければ、カーソルを暗号化するか、カーソルを保存してユーザーには不透明なキーを渡すとよいでしょう。 Slim3 では、アプリケーションは S3QueryResultList クラスを通してカーソルを使用します。このクラスは ModelQuery#asQueryResultList() によって返されます。この結果オブジェクトには getEncodedCursor() メソッドがあり、エンコードされたカーソルオブジェクトを返します。カーソルとデータ更新カーソルの位置は、結果リストの中で最後に返された結果の後の場所として定義されています。カーソルはリストの中の相対的な位置ではありません(オフセットではありません)。つまり、データストアが結果に対するインデックススキャンを開始するときに、その場所へジャンプすることができるマーカーということです。カーソルの使用と使用の間にクエリの結果が変更された場合、カーソルの後ろに出てくる変更だけがアプリケーションに通知されます。新たな結果がクエリのカーソル位置よりも前に現れる場合は、カーソルの後の結果をフェッチする時には返されません。同様に、カーソルの前に現れていたエンティティがクエリの結果に含まれなくなってもカーソルの後の結果には変更はありません。最後に返された結果が結果セットから削除されても、カーソルは次の結果をどのように見つけたらよいかをまだ知っています。 カーソルを使用した興味深いアプリケーションには、未確認の変更を持つエンティティを監視するというアプリケーションがあります。エンティティが変更される度に現在日時のタイムスタンププロパティを設定するアプリケーションの場合、アプリケーションはタイムスタンププロパティで昇順ソートされたクエリに対してカーソルを使用することで、結果リストの最後に移動したエンティティをチェックすることができます。エンティティのタイムスタンプが更新されると、カーソルを使用したクエリは更新されたエンティティを返します。もし最後にクエリを実行した後にエンティティが更新されていなければクエリは何も返さず、カーソルも移動しません。 カーソルの制限カーソルについていくつかのことが知られています。:
|