Consultas de Datastore en JDO

En este documento, se habla sobre el uso del framework de persistencia de los Objetos de datos de Java (JDO) para las consultas de App Engine Datastore. Para obtener más información general sobre las consultas, visita la página principal Consultas de Datastore.

Una consulta recupera entidades de Datastore que cumplen con un conjunto específico de condiciones. La consulta opera en entidades de un determinado similar; puede especificar filtros en los valores de propiedad, claves y principales de las entidades, y puede mostrar cero o más entidades como resultados. Una consulta también puede especificar órdenes de clasificación para secuenciar los resultados según los valores de sus propiedades. Los resultados incluyen todas las entidades que tienen al menos un valor (posiblemente nulo) por cada propiedad mencionada en los filtros y órdenes de clasificación y cuyos valores de propiedad cumplen con todos los criterios de filtro especificados. La consulta puede mostrar entidades completas, entidades proyectadas o solo claves de entidades.

Una consulta típica incluye lo siguiente:

Cuando se ejecuta una consulta, se recuperan todas las entidades de ese tipo que cumplen con todos los filtros indicados, en el orden especificado. Las consultas se ejecutan en modo de solo lectura.

Nota: Para conservar la memoria y mejorar el rendimiento, las consultas deben especificar, siempre que sea posible, un límite en la cantidad de resultados que se muestran.

Nota: El mecanismo de consulta basado en índices admite un rango amplio de consultas y es apto para la mayoría de las aplicaciones. Sin embargo, no admite algunos tipos de consultas comunes en otras tecnologías de bases de datos. En particular, las uniones y consultas agregadas no son compatibles con el motor de consultas de Datastore. Visita la página Consultas de Datastore para conocer sus limitaciones.

Consultas con JDOQL

JDO incluye un lenguaje de consulta para recuperar objetos que cumplen con un conjunto de criterios. Este lenguaje, llamado JDOQL, se refiere a los campos y clases de datos de forma directa y también incluye verificación de tipo para los parámetros y resultados de consulta. JDOQL es similar a SQL, pero es más apropiado para bases de datos orientadas a objetos como el almacén de datos de App Engine. (La implementación de la API de JDO de App Engine no es compatible de forma directa con las consultas en SQL).

La interfaz Query de JDO admite varios estilos de llamada: puedes especificar una consulta completa en una string con la sintaxis de la string JDOQL o especificar algunas o todas las partes de la consulta mediante llamadas a los métodos en el objeto Query. En el siguiente ejemplo, se muestra el estilo del método de llamada con un filtro y una orden de clasificación, que utiliza la sustitución de parámetros para el valor usado en el filtro. Los valores de argumento que se pasan al método execute() del objeto Query se reemplazan en la consulta en el orden especificado:

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

Esta es la misma consulta con la sintaxis de la string:

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

Puedes mezclar estos estilos de definición de la consulta. Por ejemplo:

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

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

Puedes volver a usar una instancia Query con diferentes valores sustituidos por los parámetros mediante múltiples llamadas al método execute(). Cada llamada realiza la consulta y muestra los resultados como una colección.

La string JDOQL admite la especificación literal de valores numéricos y de string; los otros tipos de valores deben usar la sustitución de parámetro. Los literales dentro de la string de consulta se pueden encerrar entre comillas simples (') o dobles ("). En este ejemplo, se usa un literal de string:

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

Filtros

Un filtro de propiedad especifica:

  • El nombre de una propiedad
  • Un operador de comparación
  • El valor de una propiedad
Por ejemplo:

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

La aplicación debe proporcionar el valor de la propiedad. No se puede referir a otras propiedades ni calcular en términos de estas. Una entidad satisface el filtro si tiene una propiedad con el nombre dado, cuyo valor se compara con el valor especificado en el filtro, de acuerdo con la descripción del operador de comparación.

El operador de comparación puede ser cualquiera de las siguientes opciones:

Operador Significado
== Igual que
< Menor que
<= Menor que o igual que
> Mayor que
>= Mayor que o igual que
!= No igual que

Como se describe en la página principal de Consultas, una sola consulta no puede usar los filtros de desigualdad (<, <=, >, >=, !=) en más de una propiedad. (Se permiten varios filtros de desigualdad en la misma propiedad, como la consulta de un rango de valores). Los filtros contains(), que se corresponden a los filtros IN en SQL, son compatibles con el uso de la siguiente sintaxis:

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

En realidad, el operador de desigualdad (!=) realiza dos consultas: una en la que los demás filtros no se modifican y el filtro de desigualdad se reemplaza con un filtro menor que (<), y otra en la que se reemplaza con un filtro mayor que (>). Luego, los resultados se combinan en orden. Una consulta no puede tener más de un filtro de “no igual que” y si una consulta tiene uno, no puede tener otros filtros de desigualdad.

El operador contains() también realiza varias consultas: una para cada elemento en la lista especificada, con todos los demás filtros invariables y el filtro contains() reemplazado por un filtro de igualdad (==). Los resultados se combinan en el orden de los elementos en la lista. Si una consulta tiene más de un filtro contains(), se realiza como consultas múltiples, una por cada combinación posible de los valores en las listas contains().

Una consulta única que contenga operadores de desigualdad (!=) o contains() se limita a un máximo de 30 subconsultas.

Para obtener más información sobre cómo las consultas != y contains() se traducen en varias consultas en un framework de JDO/JPA, consulta el artículo Queries with != and IN filters.

En la sintaxis de la string JDOQL, puedes separar múltiples filtros con los operadores && (“y” lógica) y || (“o” lógica):

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

La negación ("no" lógico) no se admite. Ten en cuenta también que el operador || puede emplearse solo cuando los filtros que separa tienen el mismo nombre de propiedad (es decir, cuando se pueden combinar en un solo filtro 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'");

Órdenes de clasificación

Un orden de clasificación de consulta especifica:

  • El nombre de una propiedad
  • Una dirección de clasificación (ascendente o descendente)

Por ejemplo:

// 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);

Por ejemplo:

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

Si una consulta incluye varios órdenes de clasificación, estos se aplican en la secuencia especificada. En el siguiente ejemplo, primero se clasifica por apellido ascendente, luego, por altura descendente:

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

Si no se especifica ningún orden de clasificación, los resultados se muestran en el orden en que se recuperan de Datastore.

Nota: Debido a la forma en que Datastore ejecuta las consultas, si una consulta especifica filtros de desigualdad en una propiedad y órdenes de clasificación en otras propiedades, la propiedad que se use en los filtros de desigualdad deberá ordenarse antes de las demás propiedades.

Rangos

Una consulta puede especificar un rango de resultados que se muestra en la aplicación. El rango indica qué resultados del conjunto completo de resultados se deben mostrar primero y cuáles último. Los resultados se identifican según sus índices numéricos, el 0 denota el primer resultado del conjunto. Por ejemplo, un rango de 5, 10 muestra los resultados del 6 al 10:

q.setRange(5, 10);

Nota: El uso de rangos puede afectar el rendimiento, ya que Datastore debe recuperar y, luego, descartar todos los resultados que provienen del inicio del desplazamiento. Por ejemplo, una consulta con un rango de 5, 10 recupera diez resultados de Datastore, descarta los primeros cinco y muestra los cinco restantes a la aplicación.

Consultas basadas en claves

Las claves de entidad pueden ser el sujeto de un filtro de consulta o de un orden de clasificación. Datastore considera el valor de la clave completa para esas consultas, incluida la ruta del principal de la entidad, el similar y la string del nombre de clave asignado por la aplicación o el ID numérico asignado por el sistema. Debido a que la clave es única en todas las entidades del sistema, las consultas de claves facilitan la recuperación de entidades de un similar determinado en lotes, como para un volcado de lotes de los contenidos de Datastore. A diferencia de los rangos de JDOQL esto funciona de manera eficiente para cualquier cantidad de entidades.

Cuando se usa un comparador de desigualdad, se aplican los siguientes criterios para clasificar las claves, en orden:

  1. Ruta del principal
  2. Tipo de entidad
  3. Identificador (nombre de clave o ID numérico)

Los elementos de la ruta principal se comparan de manera similar: por tipo (string) y, luego, por nombre de clave o ID numérico. Los tipos y los nombres de clave son strings y se ordenan por valor de byte. Los ID numéricos son números enteros ordenados de forma numérica. Si varias entidades que tienen el mismo superior y similar usan una combinación de ID numéricos y strings con nombre de clave, las entidades que tienen ID numéricos anteceden a las que tienen nombres de clave.

En JDO, haces referencia a la clave de la entidad en la consulta mediante el campo de clave primaria del objeto. Para usar una clave como filtro de consulta, debes especificar el tipo de parámetro Key con el método declareParameters(). La siguiente consulta busca todas las entidades Person con una comida favorita determinada y supone que hay una relación uno a uno sin propietario entre Person y Food:

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());

Una consulta de solo claves muestra solo las claves de las entidades resultantes, en lugar de las entidades en sí, con una latencia y un costo menor que recuperar entidades completas:

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

A menudo, es más rentable hacer primero consultas de solo clave y, luego, recuperar un subconjunto de entidades de los resultados, en lugar de ejecutar una consulta general que puede arrojar más entidades de las que necesitas.

Extensiones

Una extensión JDO representa cada objeto de Datastore de una clase en particular. Puedes crearla si pasas la clase que deseas al método getExtent() del administrador de persistencia. La interfaz Extent extiende la interfaz Iterable para acceder a los resultados y recuperarlos por lotes según sea necesario. Cuando termines de acceder a los resultados, llama al método closeAll() de la extensión.

En el siguiente ejemplo, se itera sobre cada objeto Person en Datastore:

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

// ...

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

Borra entidades por consulta

Si realizas una consulta con el objetivo de borrar todas las entidades que coinciden con el filtro de búsqueda, puedes guardarte algo de codificación mediante uso de la función de JDO “borrar por consulta”. La siguiente consulta borra a todas las personas de una altura determinada:

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

Notarás que la única diferencia es que estamos con esta opción se llama a q.deletePersistentAll() en lugar de a q.execute(). Todas las reglas y restricciones de los filtros que se describen arriba, las órdenes de clasificación y los índices se aplican a las consultas en las que seleccionas o borras el conjunto de resultados. Sin embargo, ten en cuenta que, al igual que si borras estas entidades Person con pm.deletePersistent(), también se borrarán los objetos secundarios dependientes de las entidades que borró la consulta. Para obtener más información sobre los objetos secundarios dependientes, visita la página Relaciones de las entidades en JDO.

Cursores de consulta

En JDO, puedes usar una extensión y la clase JDOCursorHelper para usar cursores con las consultas de JDO. Los cursores funcionan cuando se recuperan resultados como una lista o cuando se usa un iterador. Para obtener un cursor, debes pasar la lista de resultados o el iterador al método estático 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

Para obtener más información sobre los cursores de consulta, visita la página Consultas sobre Datastore

Política de lectura y plazo de llamada de Datastore

Puedes configurar la política de lectura (coherencia sólida o coherencia eventual) y el plazo de llamada a Datastore para todas las llamadas realizadas desde una instancia PersistenceManager mediante la configuración. También puedes anular estas opciones para un objeto Query individual. (Sin embargo, ten en cuenta que no hay manera de anular la configuración para estas opciones cuando recuperas entidades por clave).

Cuando se selecciona la coherencia eventual para una consulta de Datastore, también se accede a los índices que la consulta usa a fin de reunir los resultados con coherencia eventual. En ocasiones, las consultas muestran entidades que no coinciden con los criterios de consulta, aunque esto también sucede con una política de lectura muy consistente. (Si la consulta usa un filtro de principal, puedes usar transacciones para asegurarte de obtener un conjunto de resultados coherente).

A fin de anular la política de lectura para una sola consulta, llama a su método addExtension():

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

Los valores posibles son "EVENTUAL" y "STRONG". El valor predeterminado es "STRONG", a menos que se establezca lo contrario en el archivo de configuración jdoconfig.xml.

Para anular el plazo de llamada a Datastore debido a una sola consulta, llama a su método setDatastoreReadTimeoutMillis():

q.setDatastoreReadTimeoutMillis(3000);

El valor es un intervalo de tiempo expresado en milésimas de segundo.