Consultas de Datastore en JDO

En este documento se habla sobre el uso del marco de trabajo 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 cumplan con un conjunto específico de condiciones. La consulta opera en entidades de un tipo determinado; puede especificar filtros en los valores de propiedad, las claves y los 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. Consulta 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, hace referencia a los campos y clases de datos de JDO de forma directa y también incluye la verificación de tipo para los parámetros y los 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 sola instancia Query con valores diferentes reemplazados por los parámetros mediante varias 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 uno en el que se reemplazado con un filtro mayor que (>). Luego, los resultados se combinan en orden. Una consulta no puede tener más de un filtro no igualdad y una consulta que 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 con un filtro de paridad (===). 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 sola consulta que contenga operadores de desigualdad (!=) o contains() está limitada a realizar no más de 30 subconsultas.

Para obtener más información sobre cómo las consultas != y contains() se traducen en varias consultas en un marco de trabajo de JDO/JPA, consulta el artículo Consultas con filtros IN y !=.

En la sintaxis de la string JDOQL, puedes separar varios filtros con los operadores && ("y" lógico ) y || ("o" lógico):

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 recuperaron 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 el almacén de datos 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 del almacén de datos, 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. El almacén de datos considera el valor de la clave completa para esas consultas, incluida la ruta del principal de la entidad, el tipo 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 tipo determinado en lotes, como para un volcado de lotes de los contenidos de la base de datos. 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 numéricamente. Si varias entidades que tienen el mismo principal y tipo 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, refieres 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 en el método declareParameters(). A continuación, se encuentran todas las Person entidades con un alimento favorito dado, suponiendo una relación uno-uno sin dueño 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 del almacén de datos de una clase en particular. Para crearla, pasa la clase deseada al método getExtent() de Persistence Manager. La interfaz Extent extiende la interfaz Iterable para acceder a los resultados y recuperarlos en 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 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 consultas de JDO. Los cursores funcionan cuando se recuperan resultados como una lista o cuando se usa un iterador. Para obtener un cursor, pasa 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 frente a coherencia eventual) y el plazo de llamada de Datastore para todas las llamadas realizadas por una instancia PersistenceManager mediante la configuración. También puede 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). Consulta el artículo Aislamiento de transacción en App Engine para obtener más información sobre cómo se actualizan las entidades y los índices.

Para anular la política de lectura de una sola consulta, invoca 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 de 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.