クエリカーソル

クエリカーソルを使用すると、アプリケーションでクエリの結果を一括して取得できます。ページネーションには整数オフセットではなくクエリカーソルを使うことをおすすめします。アプリでのクエリの構成方法について詳しくは、クエリをご覧ください。

クエリカーソル

クエリカーソルを使用すると、クエリ オフセットのオーバーヘッドを発生させることなく、アプリケーションでクエリの結果を一括して取得できます。取得オペレーションの実行後にアプリケーションで取得できるようになるカーソルは、Base64 でエンコードされた不透明な文字列で、最後に取得された結果のインデックス位置をマーク付けします。この文字列は、アプリケーションにより Datastore、Memcache、Task Queue タスク ペイロードなどに保存されるか、HTTP の GET パラメータまたは POST パラメータとしてウェブページに埋め込まれます。その後カーソルは、後続の取得オペレーションの開始点となります。これにより、前の取得オペレーションが終了した位置から次のバッチの結果を取得できます。取得オペレーションでは終了カーソルも指定できるため、返される結果セットの範囲を制限できます。

オフセットとカーソルの違い

Datastore では整数のオフセットをサポートしていますが、使用は避けてください。その代わりにカーソルを使用します。オフセットを使用するとスキップされたエンティティがアプリケーションに返されなくなりますが、内部ではスキップされたエンティティも引き続き取得されています。スキップされたエンティティはクエリのレイテンシに影響するだけでなく、そのようなエンティティの取得に必要な読み取りオペレーションに対してアプリケーションが課金されます。このようなコストは、オフセットの代わりにカーソルを使用することですべて回避できます。

クエリカーソルの例

低レベル API では、アプリケーションで QueryResultListQueryResultIterableQueryResultIterator の各インターフェースを介してカーソルを使用できます。これらのインターフェースは、PreparedQuery メソッド asQueryResultList()asQueryResultIterable()asQueryResultIterator() によってそれぞれ返されます。返される各オブジェクトには getCursor() メソッドが用意されており、これらから Cursor オブジェクトが返されます。アプリケーションは、Cursor オブジェクトの toWebSafeString() メソッドを呼び出すことにより、カーソルを表すウェブセーフな文字列を取得できます。その後、静的メソッド Cursor.fromWebSafeString() を使用してその文字列からカーソルを再構成できます。

次の例は、ページ分けにカーソルを使う方法を示しています。


import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.appengine.api.datastore.QueryResultList;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ListPeopleServlet extends HttpServlet {

  static final int PAGE_SIZE = 15;
  private final DatastoreService datastore;

  public ListPeopleServlet() {
    datastore = DatastoreServiceFactory.getDatastoreService();
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    FetchOptions fetchOptions = FetchOptions.Builder.withLimit(PAGE_SIZE);

    // If this servlet is passed a cursor parameter, let's use it.
    String startCursor = req.getParameter("cursor");
    if (startCursor != null) {
      fetchOptions.startCursor(Cursor.fromWebSafeString(startCursor));
    }

    Query q = new Query("Person").addSort("name", SortDirection.ASCENDING);
    PreparedQuery pq = datastore.prepare(q);

    QueryResultList<Entity> results;
    try {
      results = pq.asQueryResultList(fetchOptions);
    } catch (IllegalArgumentException e) {
      // IllegalArgumentException happens when an invalid cursor is used.
      // A user could have manually entered a bad cursor in the URL or there
      // may have been an internal implementation detail change in App Engine.
      // Redirect to the page without the cursor parameter to show something
      // rather than an error.
      resp.sendRedirect("/people");
      return;
    }

    resp.setContentType("text/html");
    resp.setCharacterEncoding("UTF-8");
    PrintWriter w = resp.getWriter();
    w.println("<!DOCTYPE html>");
    w.println("<meta charset=\"utf-8\">");
    w.println("<title>Cloud Datastore Cursor Sample</title>");
    w.println("<ul>");
    for (Entity entity : results) {
      w.println("<li>" + entity.getProperty("name") + "</li>");
    }
    w.println("</ul>");

    String cursorString = results.getCursor().toWebSafeString();

    // This servlet lives at '/people'.
    w.println("<a href='/people?cursor=" + cursorString + "'>Next page</a>");
  }
}

カーソルの制限

カーソルには次のような制限があります。

  • カーソルを使用できるのは元のクエリを実行したアプリケーションと同じアプリケーションだけであり、同じクエリに対してのみ処理を継続します。後続の取得オペレーションでカーソルを使用するには、エンティティの種類、祖先フィルタ、プロパティ フィルタ、並べ替え順序を同じにするなど、元のクエリを正確に再作成する必要があります。元の生成元から同じクエリを設定しない限り、カーソルを使用して結果を取得することはできません。
  • NOT_EQUAL 演算子と IN 演算子は複数のクエリで実装されるため、これらの演算子を使用するクエリはカーソルをサポートしません。また、CompositeFilterOperator.or メソッドを使用して作成された複合クエリもカーソルをサポートしません。
  • 複数の値を持つプロパティに対して不等式フィルタまたは並べ替え順序を使用するクエリでは、カーソルが常に予期したとおりに動作するとは限りません。このような複数の値を持つプロパティに対する重複排除ロジックは取得オペレーション間で保持されないため、同じ結果が複数回返される原因となる場合があります。
  • App Engine の新しいリリースでは内部実装の詳細が変更される場合があり、それに依存するカーソルは無効になる可能性があります。無効になったカーソルをアプリケーションで使用しようとすると、Datastore は IllegalArgumentException(低レベル API)、JDOFatalUserException(JDO)、または PersistenceException(JPA)を出力します。

カーソルとデータ更新

カーソルの位置は、最後の結果が返された後の結果リスト内の位置として定義されます。カーソルはリスト内の相対位置ではありません(オフセットではありません)。結果を得るためのインデックス スキャンを開始するときに Datastore がジャンプできるマーカーです。以前のカーソル使用時からクエリの結果が変更されている場合、クエリが認識するのは、そのカーソル位置よりも後の結果で発生した変化だけです。新しい結果の位置がクエリのカーソル位置よりも前の場合、カーソルより後の結果をフェッチしても、その新しい結果は返されません。同様に、カーソルよりも前の位置に存在していたエンティティがクエリの結果ではなくなった場合、カーソルより後の位置の結果は変化しません。また最後に返された結果が結果セットから削除された場合でも、その次の結果の位置を引き続きカーソルで特定できます。

クエリの結果を取得するときは、開始カーソルと終了カーソルの両方を使用して、連続した結果グループを Datastore から返すことができます。開始カーソルと終了カーソルを使用して結果を取得する場合、結果セットのサイズがカーソル生成時と同じであることは保証されません。カーソルが生成されてからクエリで使用されるまでに、エンティティが Datastore に追加または削除される可能性があります。

次のステップ