Cursori di query

I cursori di query consentono a un'applicazione di recuperare i risultati di una query in pratici batch e sono consigliati rispetto all'uso degli offset di numeri interi per l'impaginazione. Consulta Query per ulteriori informazioni sulla struttura delle query per la tua app.

Cursori query

I cursori di query consentono a un'applicazione di recuperare i risultati di una query in pratici batch senza incorrere nell'overhead di un offset delle query. Dopo aver eseguito un'operazione di recupero, l'applicazione può ottenere un cursore, ovvero una stringa opaca con codifica Base64 che contrassegna la posizione dell'indice dell'ultimo risultato recuperato. L'applicazione può salvare questa stringa, ad esempio in Datastore, in Memcache, in un payload di attività della coda di attività oppure incorporata in una pagina web come parametro HTTP GET o POST. Può quindi utilizzare il cursore come punto di partenza per un'operazione di recupero successiva per ottenere il batch di risultati successivo dal punto in cui è terminato il recupero precedente. Un recupero può anche specificare un cursore finale, per limitare l'estensione del set di risultati restituito.

Offset e cursori

Anche se Datastore supporta gli offset di numeri interi, è consigliabile evitare di utilizzarli. Utilizza invece i cursori. L'utilizzo di un offset evita solo di restituire le entità ignorate all'applicazione, ma queste entità vengono comunque recuperate internamente. Le entità ignorate influiscono sulla latenza della query e alla tua applicazione vengono addebitate le operazioni di lettura necessarie per recuperarle. L'utilizzo di cursori anziché di offset ti consente di evitare tutti questi costi.

Esempio di cursore di query

Nell'API di basso livello, l'applicazione può utilizzare i cursori tramite le interfacce QueryResultList, QueryResultIterable e QueryResultIterator, restituite rispettivamente dai metodi PreparedQuery asQueryResultList(), asQueryResultIterable() e asQueryResultIterator(). Ciascuno di questi oggetti risultato fornisce un metodo getCursor(), che a sua volta restituisce un oggetto Cursor. L'applicazione può ottenere una stringa sicura per il web che rappresenta il cursore richiamando il metodo toWebSafeString() dell'oggetto Cursor e può utilizzare il metodo statico Cursor.fromWebSafeString() in un secondo momento per ricostituire il cursore a partire dalla stringa.

L'esempio seguente mostra l'utilizzo dei cursori per l'impaginazione:


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

Limitazioni dei cursori

I cursori sono soggetti alle seguenti limitazioni:

  • Un cursore può essere utilizzato solo dalla stessa applicazione che ha eseguito la query originale e solo per continuare la stessa query. Per utilizzare il cursore in un'operazione di recupero successiva, devi ricostituire esattamente la query originale, inclusi lo stesso tipo di entità, filtro predecessore, filtri di proprietà e ordini di ordinamento. Non è possibile recuperare i risultati utilizzando un cursore senza configurare la stessa query da cui è stato inizialmente generato.
  • Poiché gli operatori NOT_EQUAL e IN vengono implementati con più query, le query che li utilizzano non supportano i cursori, né le query composite create con il metodo CompositeFilterOperator.or.
  • I cursori non funzionano sempre come previsto in una query che utilizza un filtro di disuguaglianza o un ordinamento in una proprietà con più valori. La logica di deduplicazione di queste proprietà a più valori non persiste tra un recupero e l'altro, causando probabilmente la restituzione dello stesso risultato più volte.
  • Le nuove release di App Engine potrebbero modificare i dettagli di implementazione interni, annullando i cursori che dipendono da essi. Se un'applicazione tenta di utilizzare un cursore non più valido, Datastore genera un elemento IllegalArgumentException (API di basso livello), JDOFatalUserException (JDO) o PersistenceException (JPA).

Cursori e aggiornamenti dei dati

La posizione del cursore è definita come la posizione nell'elenco dei risultati dopo l'ultimo risultato restituito. Il cursore non è una posizione relativa nell'elenco (non rappresenta un offset); è un indicatore a cui Datastore può passare quando si avvia la scansione dell'indice per trovare i risultati. Se i risultati di una query cambiano tra gli utilizzi di un cursore, la query rileva solo le modifiche che si verificano nei risultati successivi al cursore. Se un nuovo risultato viene visualizzato prima della posizione del cursore per la query, non verrà restituito quando vengono recuperati i risultati dopo il cursore. Analogamente, se un'entità non è più un risultato per una query, ma è stata visualizzata prima del cursore, i risultati visualizzati dopo il cursore non cambiano. Se l'ultimo risultato restituito viene rimosso dall'insieme di risultati, il cursore sa comunque come individuare il risultato successivo.

Quando recuperi i risultati della query, puoi utilizzare sia un cursore di inizio sia un cursore finale per restituire un gruppo continuo di risultati da Datastore. Quando utilizzi un cursore di inizio e fine per recuperare i risultati, non hai la certezza che la dimensione dei risultati corrisponda a quella di quando hai generato i cursori. È possibile aggiungere o eliminare entità da Datastore tra il momento in cui i cursori vengono generati e il momento in cui vengono utilizzati in una query.

Che cosa succede dopo?