アプリケーションは、クエリを使用して、データストアを検索し、フィルタと呼ばれる検索基準に合致するエンティティを見つけます。
概要
アプリケーションは、クエリを使用して、データストアを検索し、フィルタと呼ばれる検索基準に合致するエンティティを見つけます。たとえば、複数のゲストブックをトラッキングするアプリケーションの場合、クエリを使用して、1 つのゲストブックのメッセージを日付順に取得できます。
...
...
クエリの中には複雑なものもあります。それらのクエリ用に、データストアのインデックスを事前に構築する必要があります。事前に構築したインデックスは、構成ファイル index.yaml で指定します。開発用サーバーでは、まだ指定していないインデックスを必要とするクエリを実行すると、開発用サーバーが自動的にそのインデックスを index.yaml に追加します。ただし、ユーザーのウェブサイトでは、未指定のインデックスを必要とするクエリは失敗します。したがって、通常の開発サイクルでは、開発用サーバーで新しいクエリを試行してから、ウェブサイトを更新して自動的に修正された index.yaml を使用します。index.yaml は、アプリケーションのアップロードとは別に、gcloud app deploy index.yaml を実行することにより更新できます。データストアに大量のエンティティがある場合、新しいインデックスを作成するのに時間がかかります。この場合、新しいインデックスを使用するコードをアップロードする前に、インデックス定義を更新することをおすすめします。管理コンソールを使用すると、インデックスの作成がいつ終了したかを検知できます。
App Engine Datastore では、完全一致(演算子 ==)と比較(演算子<、<=、>、>=)のフィルタをネイティブで使用できます。ブール AND 演算を使用して複数のフィルタを組み合わせることも可能です。ただし、制限があります(以下を参照)。
ネイティブ演算子の他にも、!= 演算子がサポートされます。ブール OR 演算子と IN 演算子を使用してフィルタのグループを組み合わせ、可能な値のリストのいずれかに対して等式をテストします(Python の「in」演算子と同じ)。これらの演算は、データストアのネイティブ演算と 1 対 1 で対応しないため、多少複雑で、処理が若干遅いです。実装は、結果ストリームのメモリ内結合を使用して行われます。p != v が「p < v OR p > v」として実装されることに注意してください(このことは反復プロパティで問題になります)。
制限: データストアにはクエリに関する制限があります。違反すると、例外が発生します。たとえば、過剰な数のフィルタを組み合わせることや、複数のプロパティに関して不等式を使用すること、異なるプロパティに対する並べ替え順序と不等式を組み合わせることなどは、現在許可されていません。また、複数のプロパティを参照するフィルタは、2 次的なインデックスの構成が必要になる場合があります。
サポート対象外: データストアは部分文字列の一致、大文字と小文字を区別しない一致、さらには全文検索を直接にはサポートしていません。大文字と小文字を区別しない一致や全文検索を実装するには、計算済みプロパティを使用します。
プロパティ値でフィルタリングする
NDB プロパティの Account クラスを思い出してください。
通常、プロパティのすべてのエンティティを取得することが必要となることはありません。そのプロパティの特定の値または特定の範囲の値のエンティティだけが必要となります。
プロパティ オブジェクトは、一部の演算子をオーバーライドし、クエリの制御に使用できるフィルタ式を返します。たとえば、userid のプロパティ値が 42 となっているすべての Account エンティティを見つけるには、次の式を使用します。
(Account エンティティの中にその userid に該当するものが 1 つのみ存在することが判明している場合は、userid をキーとして使用できます。Account.get_by_id(...) は Account.query(...).get() よりも高速です。)
NDB では、次の演算子がサポートされます。
property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])
不等式によるフィルタリングを行うには、次のような構文を使用します。
この構文では、userid プロパティが 40 以上の Account エンティティが検索されます。
このような演算のうち、!= と IN は他と組み合わせて実装します。そのため、多少複雑になります。詳細については、!= と IN をご覧ください。
複数のフィルタを指定できます。
この構文では、指定したフィルタ引数を組み合わせて、userid 値が 40 以上かつ 50 未満の Account エンティティを返します。
注: 上記で説明したとおり、データストアでは、複数のプロパティを対象に不等式のフィルタを使用するクエリは拒否されます。
単一の式でクエリフィルタ全体を指定するのではなく、段階的に式を構築することをおすすめします。たとえば、次のとおりです。
query3 は、前のサンプルの query 変数と同じです。クエリ オブジェクトは不変なので、query2 を構築しても query1 に影響せず query3 を構築しても query1 または query2 に影響しません。
!= 演算子と IN 演算子
NDB プロパティの Article クラスを思い出してください。
!=(等しくない)と IN(メンバーに含む)の演算子は、OR 演算子を使用して他のフィルタを組み合わせて実装します。まず、次の構文をご覧ください。
property != value
これは、次のように実装されます。
(property < value) OR (property > value)
次に例を示します。
これは次と同等です。
注: このクエリは、「perl」をタグとして含まない Article エンティティを検索するわけではありません。そうではなく、「perl」とは等しくないタグを 1 つ以上含むすべてのエンティティを検索します。たとえば、次のエンティティは、タグの 1 つとして「perl」を持っていますが、結果の中に含まれます。
これに対し、次のエンティティは、結果の中に含まれません。
「perl」と等しいタグを含まないエンティティを検索する方法は存在しません。
同様に、IN 演算は次のようになります。
property IN [value1, value2, ...]
これは、可能性のある値のリストのメンバーを検査します。次のように実装されます。
(property == value1) OR (property == value2) OR ...
次に例を示します。
これは次と同等です。
注: OR を使用するクエリでは、結果の重複がなくなります。エンティティが複数のサブクエリに合致している場合でも、結果ストリームに同一エンティティが複数回含まれることはありません。
反復プロパティのクエリを実行する
上記のセクションで定義された Article クラスは、反復プロパティのクエリのサンプルとしても機能します。次のフィルタをご覧ください:
Article.tags は反復プロパティですが、単一の値を使用しています。反復プロパティをリスト オブジェクトと比較することはできません(データストアは認識しません)。フィルタは以下のようになります。
上記のフィルタの場合、Article エンティティのタグ値がリスト ['python', 'ruby', 'php'] であるものが検索対象になるのではありません。そうではなく、リスト形式のエンティティの tags 値に上記の値の少なくとも 1 つが含まれているものを検索します。
反復プロパティに対して None の値のクエリを実行すると、未定義の振る舞いが生じるため、実行しないでください。
AND 演算と OR 演算を組み合わせる
必要に応じて、AND 演算と OR 演算をネストできます。次に例を示します。
しかし、OR の実装により、この形式のクエリはあまりに複雑になり、例外が発生して失敗する可能性があります。これらのフィルタを正規化して、式木の最上部に OR 演算が(最大でも)1 個あるようにし、その下の AND 演算のレベルが 1 つだけになるようにすると、安全に処理されます。
この正規化を実行する際には、ブール論理のルール、および != と IN のフィルタが実際にどのように実装されているかを忘れないようにする必要があります。
!=演算子とIN演算子をプリミティブ形式に展開します。それにより、!=はプロパティが値より < または > であるかどうかのチェックになり、INはプロパティがリストの最初の値、2 番目の値 ... 以後最後の値までの値と == であるかどうかのチェックになります。ORを内部に持つANDは、ORの内部に複数のANDがあり、それらが元のANDのオペランドに適用され、それらに対して元のORオペランドとは異なる単一のORのオペランドが使用されたものと同等です。たとえば、AND(a, b, OR(c, d))はOR(AND(a, b, c), AND(a, b, d))と同じです。ANDのオペランドがAND演算である場合、ネストされているANDのオペランドを、囲む方のANDに組み込むことができます。たとえば、AND(a, b, AND(c, d))はAND(a, b, c, d)と同じです。ORのオペランドがOR演算である場合、ネストされているORのオペランドを、囲む方のORに組み込むことができます。たとえば、OR(a, b, OR(c, d))はOR(a, b, c, d)と同じです。
上に示したステージの変形をサンプル フィルタに適用し、Python よりも簡単な表記を使用して書き表すと、次のようになります。
IN演算子と!=演算子にルール 1 を適用すると、次のようになります。AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl'))))ANDの一番内側にネストされているORにルール 2 を適用すると、次のようになります。AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))))- もう一つの
ORの中にネストされているORにルール 4 を適用すると、次のようになります。AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))) ANDの中にネストされている残りのORにルール 2 を適用すると、次のようになります。OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', AND(tags == 'php', tags < 'perl')), AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
- ネストされている残りの
ANDにルール 3 を適用すると、次のようになります。OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', tags == 'php', tags < 'perl'), AND(tags == 'python', tags == 'php', tags > 'perl'))
注意: 一部のフィルタでは、この正規化により、組合せ爆発が生じる場合があります。AND に 3 つの OR 句があり、それぞれに 2 つの基本句がある場合、正規化すると、OR に 8 つの AND 句となり、それぞれに 3 つの基本句が生じます。つまり、6 つの条件だったものが 24 個になります。
並べ替え順序を指定する
order() メソッドを使用すると、クエリが結果を返す順序を指定できます。このメソッドは、引数のリストを受け取ります。各引数は、プロパティ オブジェクト(昇順)かその否定(降順)のいずれかです。次に例を示します。
この構文は、すべての Greeting エンティティを取得し、content プロパティの値に応じて昇順に並べられます。同じ content プロパティのエンティティが続く場合、date プロパティの値に応じて降順に並べられます。複数の order() コールを使用して、同じ効果を実現できます。
注: order() を使用してフィルタを組み合わせる場合、データストアでは、一部の組み合わせが拒否されます。特に、不等式フィルタを使用する場合、最初の並べ替え順序は、フィルタと同じプロパティを指定する必要があります。また、2 次的なインデックスの構成が必要となる場合があります。
上位クエリ
上位クエリを使用することにより、データストアにしっかりと整合したクエリを作成できますが、同じ祖先をもつエンティティは毎秒 1 書き込みに制限されます。ここでは、データストアの顧客と顧客に関連付けられた購入の情報を使用した上位クエリと非上位クエリのトレードオフと構造の簡単な比較を行います。
次の非上位クエリの例では、Customer ごとに 1 つのエンティティがデータストアにあり、Purchase ごとに 1 つのエンティティがデータストアにあり、顧客を指す KeyProperty もあります。
顧客に属するすべての購入を検索するには、次のクエリを使用できます。
この場合、データストアの書き込みスループットは高いですが、得られるのは結果整合性のみです。新しい購入が追加されている場合には、入手するデータが古くなっている可能性があります。上位クエリを使用することで、このような動作を排除できます。
顧客と購入に上位クエリを使用した場合でも、2 つの別個のエンティティを使用した構造は変わりません。顧客の部分は同じです。しかし、購入を作成するときに、購入に KeyProperty() を指定する必要がなくなりました。それは、上位クエリを使用する場合、購入エンティティを作成するときに、顧客エンティティのキーを呼び出すからです。
購入にはそれぞれにキーがあり、顧客にも独自のキーがあります。ただし、各購入キーには customer_entity のキーが埋め込まれています。このとき、毎秒 1 つの祖先に付き 1 書き込みに制限されることに注意してください。次のコードが実行されると、1 つの祖先を持つエンティティが 1 つ作成されます。
特定の顧客の購入を照会するには、次のクエリを使用します。
クエリ属性
クエリ オブジェクトには、次の読み取り専用データ属性があります:
| 属性 | タイプ | デフォルト | 説明 |
|---|---|---|---|
| kind | str | None | 種類名(通常は、クラス名) |
| ancestor | Key | None | クエリに対して指定された上位エンティティ |
| filters | FilterNode | None | フィルタ式 |
| orders | Order | None | 並べ替え順序 |
クエリ オブジェクトを表示(print)すると(またはクエリ オブジェクトに対して str() または repr() を呼び出すと)、文字列が適切にフォーマットされた状態で表示されます。
構造化プロパティ値向けにフィルタリングする
クエリでは、構造化プロパティのフィールド値を対象にして直接フィルタリングできます。たとえば、住所の都市名が 'Amsterdam' であるすべての連絡先のクエリは、次のようになります。
このようなフィルタを複数組み合わせると、同一の Contact エンティティ内にあるさまざまな Address サブエンティティに合致するフィルタを作成できます。次に例を示します。
住所の都市名が 'Amsterdam' の連絡先と、通りの名前が 'Spear St' である別の住所の連絡先を見つけることができます。等式フィルタでは、単一のサブエンティティ内で複数の値を持つ結果のみを返すクエリを作成できます。
この方法を利用すると、None に等しいサブエンティティのプロパティは、クエリで無視されます。プロパティがデフォルト値の場合、クエリで無視するようにするには、明示的に None に設定する必要があります。そうでない場合、クエリには、そのプロパティ値がデフォルトと等しいことを要求するフィルタが含まれます。たとえば、Address モデルにプロパティ country があり、その値が default='us' である場合、上記のサンプルは、country が 'us' に等しい連絡先のみを返します。他の country 値の連絡先については、Address(city='San Francisco', street='Spear St',
country=None) でフィルタリングする必要があります。
サブエンティティが None に等しいプロパティ値を持っている場合、無視されます。そのため、None のサブエンティティ プロパティ値を対象にフィルタリングを行うことは意味がありません。
文字列で名前を設定したプロパティを使用する
名前が文字列で指定されているプロパティに基づいて、クエリのフィルタリングや並べ替えを行うことが必要となる場合があります。たとえば、ユーザーが tags:python のような検索クエリを入力できるようにする場合、次のようなクエリに組み込むことをおすすめします。
Article.query(Article."tags" == "python") # does NOT work
モデルが Expando である場合、フィルタで GenericProperty を使用できます。これは、Expando が動的プロパティ向けに使用するクラスです。
GenericProperty は、モデルが Expando でない場合にも機能しますが、定義されたプロパティ名のみを使用する場合、_properties クラス属性も利用できます。
getattr() を使用すると、クラスから取得できます。
相違点として、getattr() の場合はプロパティの「Python 名」を利用しますが、_properties の場合はプロパティの「データストア名」でインデックス化されます。プロパティが次のように宣言されている場合にのみ、相違点が影響します。
Python 名は title ですが、データストア名は t です。
同じことが、クエリ結果の並べ替え順序にも当てはまります。
クエリ イテレーター
クエリの進行中、その状態は、イテレーター オブジェクトに保持されます。(大半のアプリケーションにおいて、このオブジェクトを直接使用することはありません。通常は、イテレーター オブジェクトを操作するよりも、fetch(20) を呼び出す方がシンプルです。)オブジェクトの取得方法は基本的に 2 つあります。
- Python に組み込まれた
iter()関数をQueryオブジェクトに対して使用する Queryオブジェクトのiter()メソッドを呼び出す
1 番目の方法では、Python の for ループ(暗黙的に iter() 関数を呼び出します)を使用して、クエリに対してループを行います。
2 番目の方法では、Query オブジェクトの iter() メソッドを使用して、オプションをイテレーターに渡し、その振る舞いに影響を与えます。たとえば、for ループ内のキーのみのクエリを使用するには、次のように設定します。
クエリ イテレーターには、他にも役に立つメソッドがあります。
| メソッド | 説明 |
|---|---|
__iter__()
| Python のイテレーター プロトコルの一部です。 |
next()
| 次の結果を返します。存在しない場合、StopIteration の例外が発生します。 |
has_next()
| 次の next() コールが結果を返す場合、True を返します。StopIteration が発生する場合、False を返します。質問に対する回答が判明するまでブロックし、 next() を用いて結果を取得するまで結果をバッファします。 |
probably_has_next()
| has_next() と同様ですが、高速のショートカットを使用します(だたし、不正確な場合があります)。誤って「真」の結果を返すこと( next() が実際には StopIteration を発生しているのに、True を返すこと)はありますが、誤って「偽」を返すこと(next() が実際には結果を返しているのに、False を返すこと)は決してありません。 |
cursor_before()
| 最後の結果が返される直前のポイントを示すクエリカーソルを返します。 カーソルが利用できない場合、例外を返します(特に、 produce_cursors クエリ オプションが渡されない場合)。 |
cursor_after()
| 最後の結果が返された直後のポイントを示すクエリカーソルを返します。 カーソルが利用できない場合、例外を返します(特に、 produce_cursors クエリ オプションが渡されない場合)。 |
index_list()
| プライマリ、複合、種類、シングル プロパティ インデックスなど、実行したクエリで使用されたインデックスのリストを返します。 |
クエリカーソル
クエリカーソルとは、小さな不完全データ構造であり、クエリ内の再開ポイントを示します。これは、ユーザーに対して一度に結果のページを表示する場合に役立ちます。また、停止と再開が必要な長時間ジョブを処理する場合にも役立ちます。通常は、クエリの fetch_page() メソッドを使用します。この処理は fetch() と類似していますが、3 項の (results, cursor, more) を返します。返された more フラグは、より多くの結果が存在する可能性があることを示します。ユーザー インターフェースでは、このフラグを利用して、たとえば、[次のページ] ボタンまたはリンクを表示されないようにできます。次のページをリクエストするには、1 つの fetch_page() 呼び出しによって返されたカーソルを次に渡します。無効なカーソルを渡すと、BadArgumentError が発生します。検証では、値が base64 でエンコードされているかどうかだけがチェックされます。さらに必要な検証を行う必要があります。
クエリに合致するすべてのエンティティをユーザーが表示して、ページごとに一度に取得できるようにする場合、コードは次のようになります。
...
urlsafe() と Cursor(urlsafe=s) を使用してカーソルの直列化 / 非直列化を行っていることに注目してください。これにより、1 つのリクエストに応じて、レスポンスでカーソルをウェブ上のクライアントに渡し、後のリクエストでクライアントから取得し直すことができます。
注: fetch_page() メソッドは通常、それ以上結果がない場合でも、カーソルを返します。ただし、必ず返すとは限りません。返されるカーソル値が None である可能性もあります。また、more フラグがイテレーターの probably_has_next() メソッドを使用して実装されているために、次のページが空の場合でも、まれに True が返される場合があります。
一部の NDB クエリはクエリカーソルをサポートしていませんが、対応方法があります。クエリが IN、OR、または != を使用する場合、キー順で並べ替えを行わない限り、クエリ結果ではカーソルが機能しません。アプリケーションがキー順で結果の並べ替えを行わずに、fetch_page() を呼び出すと、BadArgumentError が取得されます。
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N)
がエラーを受け取った場合、
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N) に変更します。
クエリ結果を「ページング」する代わりに、クエリの iter() メソッドを使用すると、正確なポイントのカーソルを取得できます。このためには、produce_cursors=True を iter() に渡します。イテレーターが正しい位置にあるときに、その cursor_after() を呼び出すと、直後のカーソルを取得できます。(同様に cursor_before() を呼び出すと、直前のカーソルを取得できます。)cursor_after() や cursor_before() を呼び出すと、データストア呼び出しがブロックされ、クエリの一部を再実行して、バッチの中間を指し示すカーソルが抽出されます。
カーソルを使用して、クエリ結果に対し後方にページングする場合、逆引きクエリを作成します。
各エンティティ向けに関数を呼び出す(マッピング)
クエリによって返された Message エンティティに対応する Account エンティティを取得する必要がある場合、次のように設定します。
ただし、この方法は必ずしも効率的ではありません。エンティティを取得するまで待機してから、エンティティを使用します。そして、次のエンティティを待って、そのエンティティを使用します。そのため、待機時間が長くなります。改善策として、クエリ結果に対してマッピングされるコールバック関数を設定する方法があります。
このバージョンは、同時実行が可能なため、上記のシンプルな for ループよりもいくらか高速に実行されます。ただし、callback() の get() コールは、この場合でも同期的に処理されるため、それほど大きな利点とはなりません。このような場合は、非同期の get を使用することをおすすめします。
GQL
GQL は、App Engine Datastore からエンティティやキーを取得するための SQL に似た言語です。GQL の機能は従来のリレーショナル データベースで使用されるクエリ言語の機能とは異なりますが、GQL の構文は SQL の構文と似ています。GQL 構文については、GQL リファレンスで説明されています。
GQL を使用して、クエリを構築できます。Model.query() でクエリを作成する場合と似ていますが、GQL 構文を使用する場合は、クエリのフィルタや順序を定義します。次のように使用します。
ndb.gql(querystring)はQueryオブジェクトを返します(Model.query()によって返されるタイプと同じです)。通常のメソッドは、すべてQueryオブジェクトで利用できます(fetch()、map_async()、filter()など)。Model.gql(querystring)はndb.gql("SELECT * FROM Model " + querystring)の省略形です。通常、querystring は"WHERE prop1 > 0 AND prop2 = TRUE"のような形です。- 構造化プロパティを含むモデルのクエリを行うには、
foo.barを GQL 構文内で使用してサブプロパティを参照します。 - GQL では、SQL のようなパラメータ バインディングをサポートします。アプリケーションは、クエリを定義して、それに値をバインディングできます。または
クエリの
bind()関数を呼び出すと、新しいクエリが返されます。元のクエリは変更されません。 - モデルクラスが
_get_kind()クラスメソッドをオーバーライドする場合、GQL クエリは、その関数によって返された種類を使用し、クラス名は使用しません。 - モデルのプロパティがその名前をオーバーライドする場合(
foo = StringProperty('bar')など)GQL クエリはオーバーライドされたプロパティ名を使用します(例の中ではbar)。
クエリ内の値がユーザー指定変数である場合は、必ずパラメータ バインディング機能を使用します。このようにすることで、構文ハックによる攻撃を回避できます。
インポートされていないモデル(一般的には、定義されていないモデル)のクエリを行うと、エラーが発生します。
モデルクラスによって定義されていないプロパティ名を使用すると、そのモデルが Expando でない限り、エラーが発生します。
クエリの fetch() に対して制限やオフセットを指定すると、GQL の OFFSET 句や LIMIT 句によって設定された制限やオフセットをオーバーライドします。GQL の OFFSET と LIMIT を fetch_page() と結合しないでください。クエリで App Engine によって適用される最大 1,000 件の結果は、オフセットと制限の両方に適用されます。
SQL ユーザーの場合、GQL を使用する際に誤った前提に立たないようにご注意ください。GQL は、NDB のネイティブ クエリ API に翻訳されます。この点は、通常のオブジェクト リレーショナル マッパー(SQLAlchemy や Django のデータベース サポートなど)とは異なります。オブジェクト リレーショナル マッパーでは、API コールが SQL に翻訳されてから、データベース サーバーに送信されます。GQL では、データストアの修正(挿入、削除、更新)はサポートされていません。クエリのみがサポートされています。