쿼리 커서

쿼리 커서를 사용하면 애플리케이션이 쿼리 결과를 편리하게 일괄적으로 검색할 수 있으며, 정수 오프셋으로 페이지를 나누는 방식보다 쿼리 커서가 권장됩니다. 앱의 쿼리 구조화에 대한 자세한 내용은 쿼리를 참조하세요.

쿼리 커서

쿼리 커서를 사용하면 쿼리 오프셋의 오버헤드 발생 없이 애플리케이션에서 편리하게 일괄적으로 쿼리 결과를 검색할 수 있습니다. 검색 작업을 수행한 후 애플리케이션은 커서를 가져올 수 있습니다. 커서는 마지막으로 검색된 결과의 색인 위치를 표시하는 불투명한 base64 인코딩 문자열입니다. 애플리케이션은 이 문자열을 Cloud Datastore, Memcache, 작업 대기열 작업 페이로드 등에 저장하거나 웹페이지에 HTTP GET 또는 POST 매개변수로 포함한 후 해당 커서를 후속 검색 작업의 시작점으로 사용하여 이전 검색이 끝난 지점부터 결과의 다음 배치를 가져올 수 있습니다. 검색 시 끝 커서를 지정하여 결과 집합이 반환되는 범위를 제한할 수도 있습니다.

오프셋과 커서 비교

Cloud Datastore가 정수 오프셋을 지원하지만 사용하지 마세요. 대신 커서를 사용하세요. 오프셋을 사용하면 건너뛴 항목이 애플리케이션에 반환되지는 않지만 내부적으로는 계속 검색됩니다. 건너뛴 항목은 쿼리의 지연 시간에 영향을 주며, 애플리케이션이 검색하는 데 소요된 읽기 작업에 대해 비용이 청구됩니다. 오프셋 대신 커서를 사용하면 이러한 모든 비용이 청구되지 않습니다.

쿼리 커서 예시

저수준 API에서 애플리케이션은 QueryResultList, QueryResultIterable, QueryResultIterator 인터페이스를 통해 커서를 사용할 수 있으며, 커서는 각각 PreparedQuery 메서드 asQueryResultList(), asQueryResultIterable(), asQueryResultIterator()에 의해 반환됩니다. 이러한 각 결과 객체는 getCursor() 메서드를 제공하고, 이 메서드는 다시 Cursor 객체를 반환합니다. 애플리케이션은 Cursor 객체의 toWebSafeString() 메서드를 호출하여 커서를 나타내는 웹 안전 문자열을 가져올 수 있으며, 나중에 정적 메서드 Cursor.fromWebSafeString()을 사용하여 이 문자열로부터 커서를 다시 구성할 수 있습니다.

다음 예시에서는 페이지 매김을 위한 커서 사용을 보여 줍니다.

자바 8


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>");
  }
}

자바 7


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_EQUALIN 연산자는 여러 쿼리로 구현되므로, 이러한 연산자를 사용하는 쿼리는 커서를 지원하지 않으며 CompositeFilterOperator.or 메서드로 생성된 복합 쿼리도 커서를 지원하지 않습니다.
  • 커서는 값이 여러 개인 속성에 불일치 필터나 정렬 순서를 사용하는 쿼리에서 예상대로 작동하지 않을 수도 있습니다. 이러한 다중 값 속성에 대한 중복 제거 로직은 다음 검색까지 지속되지 않으므로 동일한 결과가 한 번 이상 반환될 수 있습니다.
  • 새로운 App Engine 출시 버전에서 내부 구현의 세부정보가 변경되면 여기에 의존하는 커서가 무효화될 수 있습니다. 애플리케이션이 더 이상 유효하지 않은 커서를 사용하려고 하면 Cloud Datastore에서 IllegalArgumentException(저수준 API), JDOFatalUserException(JDO) 또는 PersistenceException(JPA)을 발생시킵니다.

커서 및 데이터 업데이트

커서 위치는 마지막 결과가 반환된 후 결과 목록의 위치로 정의됩니다. 커서는 목록의 상대 위치, 즉 오프셋이 아니며 결과의 색인 검색 시작 시 Cloud Datastore가 이동할 수 있는 마커입니다. 커서를 사용하는 시점 간에 쿼리 결과가 변경되면 쿼리는 커서보다 뒤에 있는 결과에서 발생한 변경 사항만 알립니다. 쿼리의 커서 위치보다 앞에서 나타난 새 결과는 커서보다 뒤에 있는 결과를 가져오면 반환되지 않습니다. 마찬가지로, 항목이 더 이상 쿼리의 결과가 아니지만 커서보다 앞에 나타난 경우 커서보다 뒤에 나타나는 결과는 바뀌지 않습니다. 마지막으로 반환된 결과가 결과 집합에서 삭제된 경우, 커서는 다음 결과를 계속 찾을 수 있습니다.

쿼리 결과 검색 시 시작 커서와 끝 커서를 모두 사용하여 Cloud Datastore에서 연속적인 결과 그룹을 반환할 수 있습니다. 시작 커서와 끝 커서를 사용하여 결과 검색 시 결과의 크기가 커서 생성 시점과 동일하다는 보장은 없습니다. 커서 생성 시점과 커서가 쿼리에 사용된 시점 사이에 Cloud Datastore에서 항목이 추가되거나 삭제될 수 있습니다.

다음 단계

이 페이지가 도움이 되었나요? 평가를 부탁드립니다.

다음에 대한 의견 보내기...

자바 8용 App Engine 표준 환경