JDO での Datastore クエリ

このドキュメントでは、主に App Engine Datastore クエリ用 Java Data Objects(JDO)永続化フレームワークの使用法について説明します。クエリの一般的な詳細については、Datastore のクエリをご覧ください。

クエリは、Datastore から、指定した一連の条件を満たすエンティティを取得します。クエリは特定の種類のエンティティに対して動作します。クエリでは、エンティティのプロパティ値、キー、祖先に対してフィルタを指定でき、任意の数のエンティティを結果として返す(または返さない)ことができます。また、並べ替え順を指定し、プロパティ値を基準にして結果を並べ替えることもできます。結果に含まれるエンティティは、フィルタと並べ替え順で指定されたすべてのプロパティに値が 1 つ以上存在し(null も可)、プロパティ値がすべてのフィルタ条件を満たしたものすべてです。クエリはエンティティ全体や射影されたエンティティを返すこともできれば、エンティティのキーだけを返すこともできます。

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

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

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

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

JDOQL を使用したクエリ

JDO にはクエリ言語が用意されており、条件を指定してそれを満たすオブジェクトを取得できます。この言語は JDOQL と呼ばれ、JDO のデータクラスとフィールドを直接参照し、クエリ パラメータと結果をチェックする型のチェックを含みます。JDOQL は SQL と似ていますが、App Engine Datastore のようなオブジェクト指向のデータベースにより適しています(JDO API の App Engine の実装では、SQL クエリを直接サポートしていません)。

JDO の Query インターフェースではいくつかの呼び出しのスタイルがサポートされているので、JDOQL 文字列構文を使用して文字列で完全なクエリを指定できます。また、Query オブジェクトでメソッドを呼び出してクエリの一部またはすべてを指定することもできます。次の例は、呼び出しのメソッド スタイルを示しています。1 つのフィルタと 1 つの並べ替え順序を指定し、フィルタで使用される値にパラメータ置換を使用しています。Query オブジェクトの execute() メソッドに渡される引数値は、指定された順でクエリに代入されます。

import java.util.List;
import javax.jdo.Query;

// ...

Query q = pm.newQuery(Person.class);
q.setFilter("lastName == lastNameParam");
q.setOrdering("height desc");
q.declareParameters("String lastNameParam");

try {
  List<Person> results = (List<Person>) q.execute("Smith");
  if (!results.isEmpty()) {
    for (Person p : results) {
      // Process result p
    }
  } else {
    // Handle "no results" case
  }
} finally {
  q.closeAll();
}

以下は文字列構文を使用した同じクエリです。

Query q = pm.newQuery("select from Person " +
                      "where lastName == lastNameParam " +
                      "parameters String lastNameParam " +
                      "order by height desc");

List<Person> results = (List<Person>) q.execute("Smith");

2 つの方式を組み合わせてクエリを定義することもできます。次に例を示します。

Query q = pm.newQuery(Person.class,
                      "lastName == lastNameParam order by height desc");
q.declareParameters("String lastNameParam");

List<Person> results = (List<Person>) q.execute("Smith");

execute() メソッドを複数回呼び出してパラメータに異なる値を代入し、1 つの Query インスタンスを再利用できます。呼び出しごとにクエリが実行され、結果がコレクションとして返されます。

JDOQL 文字列構文は文字列と数値のリテラル指定をサポートし、他の値の型はすべてパラメータ置換を使用する必要があります。クエリ文字列内のリテラルは、一重引用符(')または二重引用符(")で囲むことができます。次に、文字列リテラルを使用した例を示します。

Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' order by height desc");

フィルタ

「プロパティ フィルタ」では、次の要素を指定します。

  • プロパティ名
  • 比較演算子
  • プロパティ値
例:

Filter propertyFilter =
    new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
Query q = new Query("Person").setFilter(propertyFilter);
Query q = pm.newQuery(Person.class);
q.setFilter("height <= maxHeight");

プロパティ値はアプリケーションで提供する必要があります。他のプロパティを参照することも、他のプロパティを基準として計算することもできません。エンティティが指定された名前のプロパティを持ち、その値がフィルタで指定された値との比較演算子で示された条件に合致する場合、そのエンティティはフィルタを満たすものとみなされます。

次の比較演算子を使用できます。

演算子 意味
== 等しい
< 次の値より小さい
<= 次の値以下
> 次の値より大きい
>= 次の値以上
!= 等しくない

メインのクエリに関するページで説明したように、1 つのクエリでは、複数のプロパティに対する不等式フィルタ(<<=>>=!=)を使用することはできません(値の範囲をクエリするなど、同じプロパティで複数の不等式フィルタが使用できます)。SQL の IN フィルタに対応する contains() フィルタは、次の構文を使用してサポートされます。

// Query for all persons with lastName equal to Smith or Jones
Query q = pm.newQuery(Person.class, ":p.contains(lastName)");
q.execute(Arrays.asList("Smith", "Jones"));

「等しくない」(!=)演算子では、実際には 2 つのクエリが実行されます。一方のクエリでは、他のすべてのフィルタは変更せずに、「等しくない」フィルタを「より小さい」(<)フィルタで置き換え、もう一方のクエリでは「等しくない」フィルタを「より大きい」(>)フィルタで置き換えます。それらの結果が適切な順序でマージされます。1 つのクエリに複数の「等しくない」フィルタを含めることはできません。またそのフィルタを含むクエリに別の不等式フィルタを含めることもできません。

contains() 演算子も複数のクエリを実行します。具体的には、指定されたリストに含まれる項目ごとに、他のすべてのフィルタは変更せずに contains() フィルタを「等しい」(==)フィルタで置き換えたクエリを実行します。それらの結果が、リスト内の項目の順にマージされます。クエリに複数の contains() フィルタが含まれる場合は、contains() リスト内の値の可能な組み合わせごとにクエリが繰り返されます。

「等しくない」(!=)または contains() 演算子が含まれる 1 つのクエリでは、サブクエリの数が 30 個以下に制限されます。

!=contains() を含むクエリが JDO / JPA フレームワークで複数のクエリに変換される仕組みについては、!= および IN フィルタを使用したクエリをご覧ください。

JDOQL 文字列構文では、複数のフィルタを &&(論理式の「and」)演算子や ||(論理式の「or」)演算子で区切ることができます。

q.setFilter("lastName == 'Smith' && height < maxHeight");

否定(論理式の「not」)はサポートされていません。また、|| 演算子は、それが分離するすべてのフィルタに同じプロパティ名がある場合(つまり、それらのフィルタを 1 つの contains() フィルタに結合できる場合)にのみ使用できる点にも注意してください。

// Legal: all filters separated by || are on the same property
Query q = pm.newQuery(Person.class,
                      "(lastName == 'Smith' || lastName == 'Jones')" +
                      " && firstName == 'Harold'");

// Not legal: filters separated by || are on different properties
Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' || firstName == 'Harold'");

並べ替え順序

クエリの並べ替え順序では、次の要素を指定します。

  • プロパティ名
  • 並べ替えの方向(昇順または降順)

例:

// Order alphabetically by last name:
Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING);

// Order by height, tallest to shortest:
Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING);

例:

// Order alphabetically by last name:
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc");

// Order by height, tallest to shortest:
Query q = pm.newQuery(Person.class);
q.setOrdering("height desc");

クエリに複数の並べ替え順序が含まれている場合は、指定されている順序で適用されます。次の例では、最初に名字の昇順で並べ替えてから、身長の降順で並べ替えます。

Query q =
    new Query("Person")
        .addSort("lastName", SortDirection.ASCENDING)
        .addSort("height", SortDirection.DESCENDING);
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc, height desc");

並べ替え順序が指定されていない場合、結果は Datastore から取得された順序で返されます。

注: あるプロパティに対して不等式フィルタを指定し、別のプロパティに対して並べ替え順序を指定する場合は、不等式フィルタで使用されているプロパティを他のプロパティよりも前に並べる必要があります。これは Datastore がクエリを実行する仕組みに原因があります。

範囲

アプリケーションに返される結果の「範囲」をクエリで指定できます。範囲は、最初と最後に返される完全な結果セットの結果を示します。結果は数値のインデックスで識別され、0 がセット内の最初の結果を示します。たとえば、5, 10 の範囲は 6 番目から 10 番目までの結果を返します。

q.setRange(5, 10);

注: 範囲の使用はパフォーマンスに影響する可能性があります。データストアでは開始オフセットの前にあるすべての結果を取得して破棄する必要があるためです。たとえば、5, 10 の範囲を使用するクエリは、データストアから 10 の結果を取得して、最初から 5 番目までの結果を破棄し、残りの 5 つをアプリケーションに返します。

キーベースのクエリ

エンティティ キーは、クエリのフィルタや並べ替え順序に使用できます。データストアは、このようなクエリに対して、エンティティの祖先パス、種類、アプリケーションが割り当てたキー名の文字列やシステムが割り当てた数値 ID を含む完全なキー値を検討します。キーはシステム内のすべてのエンティティに対して一意であるため、キーのクエリを実行すると、Datastore のコンテンツの一括ダンプの場合など、特定の種類のエンティティを一括で簡単に取得できます。JDOQL の範囲とは異なり、任意の数のエンティティに対して効率的に動作します。

異なる要素を比較する場合、キーは次の条件に従って次の順に順序付けられます。

  1. 祖先パス
  2. エンティティの種類
  3. 識別子(キー名または数値 ID)

祖先パスの各要素も同様に比較され、最初に種類(文字列)、次にキー名または数値 ID を基準として比較されます。種類とキー名は文字列であり、バイト値を基準として順序付けられます。また数値 ID は整数値であり、その数値の順に順序付けられます。同じ親と種類を持つエンティティが文字列のキー名と数値 ID を混用している場合、数値 ID を持つエンティティのほうがキー名を持つエンティティよりも優先されます。

JDO では、オブジェクトの主キーフィールドを使用してクエリ内のエンティティ キーを参照します。キーをクエリフィルタとして使用するには、パラメータの型 Key declareParameters() メソッドに指定します。以下の例では、PersonFood の間に 1 対 1 の非所有関係があるものとして、指定された食品を好物とするすべての Person エンティティを検索しています。

Food chocolate = /*...*/;

Query q = pm.newQuery(Person.class);
q.setFilter("favoriteFood == favoriteFoodParam");
q.declareParameters(Key.class.getName() + " favoriteFoodParam");

List<Person> chocolateLovers = (List<Person>) q.execute(chocolate.getKey());

「キーのみのクエリ」は、エンティティ自体ではなく、結果のエンティティのキーのみを返すため、エンティティ全体を取得するよりも、レイテンシとコストを抑えられます。

Query q = new Query("Person").setKeysOnly();
Query q = pm.newQuery("select id from " + Person.class.getName());
List<String> ids = (List<String>) q.execute();

一般的なクエリを実行すると、実際に必要なものよりも多くのエンティティがフェッチされる場合があります。これに対して、まずキーのみのクエリを実行し、その結果からエンティティのサブセットをフェッチしたほうが、経済的なことが少なくありません。

Extent

JDO の「extent」は、特定のクラスのデータストア内のすべてのオブジェクトを表します。extent を作成するには、必要なクラスを Persistence Manager の getExtent() メソッドに渡します。Extent インターフェースは Iterable インターフェースを拡張して結果にアクセスし、必要に応じて結果を一括で取得します。結果へのアクセスが完了したら、extent の closeAll() メソッドを呼び出します。

次に、Datastore 内のすべての Person オブジェクトを対象に処理を繰り返す例を示します。

import java.util.Iterator;
import javax.jdo.Extent;

// ...

Extent<Person> extent = pm.getExtent(Person.class, false);
for (Person p : extent) {
  // ...
}
extent.closeAll();

クエリによるエンティティの削除

クエリフィルタに一致するすべてのエンティティを削除する目的でクエリを実行している場合は、JDO の「クエリによる削除」機能を使用すると、コーディングの手間をいくらか削減できます。次の例では、指定した身長を超えるすべての人を削除しています。

Query q = pm.newQuery(Person.class);
q.setFilter("height > maxHeightParam");
q.declareParameters("int maxHeightParam");
q.deletePersistentAll(maxHeight);

ここでの唯一の違いは、q.execute() ではなく q.deletePersistentAll() を呼び出していることです。上記で説明したフィルタ、並べ替え順序、インデックスに関連する規則や制約は、結果セットを選択するか削除するかにかかわらずすべて適用されます。 pm.deletePersistent() を使用してこれらの Person エンティティを削除した場合と同様に、クエリで削除されたエンティティに依存する子もすべて削除されることに注意してください。依存する子の詳細については、JDO でのエンティティの関係をご覧ください。

クエリカーソル

JDO では、拡張機能と JDOCursorHelper クラスを使用してカーソルを JDO クエリで使用できます。カーソルは、結果をリストとして取得する場合や、イテレータを使用する場合に便利です。カーソルを取得するには、次のように結果のリストまたはイテレータを静的メソッド JDOCursorHelper.getCursor() に渡します。

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jdo.Query;
import com.google.appengine.api.datastore.Cursor;
import org.datanucleus.store.appengine.query.JDOCursorHelper;


Query q = pm.newQuery(Person.class);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the first 20 results

Cursor cursor = JDOCursorHelper.getCursor(results);
String cursorString = cursor.toWebSafeString();
// Store the cursorString

// ...

// Query q = the same query that produced the cursor
// String cursorString = the string from storage
Cursor cursor = Cursor.fromWebSafeString(cursorString);
Map<String, Object> extensionMap = new HashMap<String, Object>();
extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
q.setExtensions(extensionMap);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the next 20 results

クエリカーソルの詳細については、データストアのクエリページをご覧ください。

読み取りポリシーとデータストア呼び出し期限

読み取りポリシー(強整合性か結果整合性)と、PersistenceManager インスタンスによって実行されるすべての呼び出しに対するデータストア呼び出し期限を、構成設定を使用して設定できます。また、個々の Query オブジェクトでこれらのオプションをオーバーライドすることもできます(ただし、キーによってエンティティをフェッチする場合は、これらのオプションの構成をオーバーライドできませんので注意してください)。

Datastore クエリで結果整合性が選択されている場合は、クエリが結果の収集に使用するインデックスへも結果整合性が適用されます。クエリ条件に合致しないエンティティが返されることがありますが、これは強整合性の読み取りポリシーでも同じです(クエリで祖先フィルタを使用している場合は、トランザクションを使用して結果セットの整合性を維持できます)。

1 つのクエリの読み取りポリシーをオーバーライドするには、その addExtension() メソッドを呼び出します。

Query q = pm.newQuery(Person.class);
q.addExtension("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

指定できる値は "EVENTUAL" または "STRONG" です。構成ファイル jdoconfig.xml に何も指定されていない場合のデフォルトは "STRONG" です。

1 つのクエリのデータストア呼び出し期限をオーバーライドするには、その setDatastoreReadTimeoutMillis() メソッドを呼び出します。

q.setDatastoreReadTimeoutMillis(3000);

値はミリ秒単位の時間です。