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 Cloud Datastore que cumplen con un conjunto específico de condiciones. La consulta opera sobre entidades de una categoría determinada; puede especificar filtros en los valores de la propiedad, las claves y los principales de las entidades, y puede mostrar cero o más entidades como resultado. 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, esta recupera todas las entidades de ese tipo que satisfacen todos los filtros indicados, en el orden que se especificó. 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, este mecanismo no admite algunos tipos de consulta que son comunes en otras tecnologías de base de datos. En particular, el motor de consultas de Cloud Datastore no admite las operaciones de unión o agregación. Consulta la página Consultas sobre Datastore para obtener más información sobre las limitaciones de las consultas de Cloud Datastore.

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 de JDO Query admite varios estados 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 del argumento que se pasan al método Query del objeto execute() se sustituyen 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 de la string de consulta se pueden escribir 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:

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 Consultas, una consulta sola no puede usar los filtros de desigualdad (<, <=, >, >=, !=) en más de una propiedad. (Se permiten múltiples filtros de desigualdad en la misma propiedad, como consultar por un rango de valores). Los filtros contains() que corresponden a los filtros IN en SQL son compatibles con 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"));

El operador de desigualdad (!=) lleva a cabo dos consultas: una en la que todos los demás filtros permanecen sin cambios y el filtro de desigualdad se reemplaza con un filtro menor que (<), y otra donde se reemplaza con un filtro mayor que (>). Luego, los resultados se combinan en orden. Una consulta no puede tener más de un filtro no igual a y una consulta que tiene uno, no puede tener otros filtros de desigualdad.

El operador contains() también realiza consultas múltiples: una para cada elemento en la lista especificada con todos los demás filtros que permanecen sin cambios 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 ejecuta como varias consultas, una por cada combinación posible de los valores en las listas contains().

Cada consulta con operadores de desigualdad (!=) o contains() está limitada 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 marco de trabajo JDO/JPA, lee el artículo Consultas con filtros != y filtros IN.

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. También, ten en cuenta que el operador || solo se puede usar cuando todos 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:

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 y, luego, por altura descendente:

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 en Cloud Datastore.

Nota: Debido a la forma en que Cloud 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)

La comparación de los elementos de la ruta del principal es similar: por tipo (string), luego, por nombre de clave o ID numérico. Los tipos y los nombres de clave son strings que se ordenan por valor de byte. Los ID numéricos son valores enteros y se ordenan 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 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 = 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. 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 accedas a los resultados, llama al método closeAll() de la extensión.

En este ejemplo, se itera sobre el objeto Person en el almacén de datos:

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 aquí es que llamamos 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 eliminas las entidades Person con pm.deletePersistent(), también se eliminará cualquier objeto secundario dependiente 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 frente a coherencia eventual) y el plazo de llamada a Datastore para todas las llamadas realizadas desde una instancia PersistenceManager con la configuración. También puedes anular estas opciones para un objeto Query. (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.

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

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

Los valores posibles son "EVENTUAL" y "STRONG"; el predeterminado es "STRONG", a menos que se modifique en el archivo de configuración jdoconfig.xml.

A fin de anular el plazo de llamada al almacén de datos para 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.