Consultas de Datastore en JDO

Este documento se centra en el uso del framework de persistencia Java Data Objects (JDO) para las consultas de Datastore de App Engine. Para obtener información más general sobre las consultas, consulta la página principal Consultas de Datastore.

Una consulta obtiene entidades de Datastore que cumplen un conjunto de condiciones especificado. La consulta opera en entidades de un tipo determinado. Puede especificar filtros en los valores de las propiedades, las claves y los antecesores de las entidades, y puede devolver cero o más entidades como resultados. Una consulta también puede especificar órdenes de clasificación para secuenciar los resultados por los valores de sus propiedades. Los resultados incluyen todas las entidades que tienen al menos un valor (posiblemente nulo) para cada propiedad cuyo nombre se indica en los filtros y los criterios de ordenación, y cuyos valores de propiedad cumplen todos los criterios de filtro especificados. La consulta puede devolver entidades completas, entidades proyectadas o solo claves de entidades.

Una consulta típica incluye lo siguiente:

  • Un tipo de entidad al que se aplica la consulta
  • Cero o más filtros basados en los valores de las propiedades, las claves y los antecesores de las entidades
  • Cero o más órdenes de ordenación para secuenciar los resultados
Cuando se ejecuta, la consulta obtiene todas las entidades del tipo indicado que cumplen todos los filtros especificados, ordenadas según el orden indicado. Las consultas se ejecutan como de solo lectura.

Nota: Para ahorrar memoria y mejorar el rendimiento, las consultas deben especificar, siempre que sea posible, un límite en el número de resultados devueltos.

Nota: El mecanismo de consulta basado en índices admite una amplia gama de consultas y es adecuado para la mayoría de las aplicaciones. Sin embargo, no admite algunos tipos de consultas habituales en otras tecnologías de bases de datos. En concreto, las combinaciones y las consultas de agregación no se admiten en el motor de consultas de Datastore. Consulta la página Consultas de Datastore para ver las limitaciones de las consultas de Datastore.

Consultas con JDOQL

JDO incluye un lenguaje de consulta para recuperar objetos que cumplan un conjunto de criterios. Este lenguaje, llamado JDOQL, hace referencia directamente a las clases y los campos de datos de JDO, e incluye la comprobación de tipos para los parámetros y los resultados de las consultas. JDOQL es similar a SQL, pero es más adecuado para bases de datos orientadas a objetos, como Datastore de App Engine. La implementación de la API JDO de App Engine no admite consultas SQL directamente.

La interfaz JDO Query admite varios estilos de llamada: puede especificar una consulta completa en una cadena, usando la sintaxis de cadena JDOQL, o puede especificar algunas o todas las partes de la consulta llamando a métodos en el objeto Query. En el siguiente ejemplo se muestra el estilo de llamada de método, con un filtro y un orden, usando la sustitución de parámetros para el valor usado en el filtro. Los valores de los argumentos que se pasan al objeto Query del método 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();
}

A continuación, incluimos la misma consulta usando la sintaxis de cadenas:

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

Se pueden combinar estos estilos de definición de consultas. 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 reutilizar una sola instancia de Query con diferentes valores sustituidos por los parámetros llamando al método execute() varias veces. Cada llamada realiza la consulta y devuelve los resultados como una colección.

La sintaxis de la cadena JDOQL admite la especificación literal de cadenas y valores numéricos. Todos los demás tipos de valor deben usar la sustitución de parámetros. Los literales de la cadena de consulta pueden incluirse entre comillas simples (') o dobles ("). A continuación, se muestra un ejemplo en el que se usa un literal de cadena:

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

Filtros

Un filtro de propiedad especifica

  • Nombre de una propiedad
  • Un operador de comparación
  • Un valor de 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 puede hacer referencia a otras propiedades ni calcularse en función de ellas. Una entidad cumple las condiciones del filtro si tiene una propiedad con el nombre indicado cuyo valor se compara con el valor especificado en el filtro de la forma descrita por el operador de comparación.

El operador de comparación puede ser cualquiera de los siguientes:

Operador Significado
== Igual a
< Inferior a
<= Inferior o igual a
> Superior a
>= Superior o igual a
!= No igual a

Como se describe en la página principal Consultas, una sola consulta no puede usar filtros de desigualdad (<, <=, >, >=, !=) en más de una propiedad. Se permiten varios filtros de desigualdad en la misma propiedad, como las consultas de un intervalo de valores. Los filtros contains(), que corresponden a los filtros IN de SQL, se admiten 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 distinto de (!=) realiza dos consultas: una en la que todos los demás filtros no cambian y el filtro distinto de se sustituye por un filtro menor que (<) y otra en la que se sustituye por un filtro mayor que (>) . Después, los resultados se combinan en orden. Una consulta no puede tener más de un filtro distinto y, si tiene uno, no puede tener ningún otro filtro de desigualdad.

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

Una sola consulta que contenga los operadores distinto de (!=) o contains() puede incluir un máximo de 30 subconsultas.

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

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

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

No se admite la negación (operador lógico "no"). Ten en cuenta también 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

El orden de clasificación de una consulta especifica

  • Nombre de una propiedad
  • El orden 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 criterios de ordenación, se aplican en la secuencia especificada. En el ejemplo siguiente, se ordena primero por apellido en orden ascendente y, después, por altura en orden 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, los resultados se devuelven 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 criterios de ordenación en otras propiedades, la propiedad utilizada en los filtros de desigualdad debe ordenarse antes que las demás propiedades.

Cocinas

Una consulta puede especificar un intervalo de resultados que se devuelvan a la aplicación. El intervalo indica qué resultados del conjunto de resultados completo deben ser el primero y el último que se devuelvan. Los resultados se identifican por sus índices numéricos, y 0 indica el primer resultado del conjunto. Por ejemplo, un intervalo de 5, 10 devuelve los resultados del 6.º al 10.º:

q.setRange(5, 10);

Nota: El uso de intervalos puede afectar al rendimiento, ya que Datastore debe recuperar y, a continuación, descartar todos los resultados anteriores al desplazamiento inicial. Por ejemplo, una consulta con un intervalo de 5, 10 obtiene diez resultados de Datastore, descarta los cinco primeros y devuelve los cinco restantes a la aplicación.

Consultas basadas en claves

Las claves de entidad pueden estar sujetas a un filtro o al criterio de ordenación de una consulta. Datastore tiene en cuenta el valor de clave completo de estas consultas, incluido el ancestro de la entidad, el tipo y la cadena de nombre de clave asignada por la aplicación o el ID numérico asignado por el sistema. Como la clave es única en todas las entidades del sistema, las consultas de clave facilitan la recuperación de entidades de un tipo determinado en lotes, como en el caso de un volcado por lotes del contenido de Datastore. A diferencia de los intervalos de JDOQL, funciona de forma eficiente con cualquier número de entidades.

Al comparar desigualdades, las claves se ordenan según los siguientes criterios:

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

Los elementos de la ruta de ancestros se comparan de forma similar: por tipo (cadena) y, a continuación, por nombre de clave o ID numérico. Los tipos y los nombres de clave son cadenas y se ordenan por valor de byte. Los IDs numéricos son números enteros y se ordenan numéricamente. Si las entidades con el mismo elemento superior y tipo usan una combinación de cadenas de nombres de clave e IDs numéricos, las que tienen IDs numéricos preceden a las que tienen nombres de clave.

En JDO, se hace referencia a la clave de la entidad en la consulta mediante el campo de clave principal del objeto. Para usar una clave como filtro de consulta, especifica el tipo de parámetro Key en el método declareParameters(). La siguiente consulta busca todas las entidades Person con una comida favorita determinada, suponiendo que hay una relación individual no propia 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 devuelve solo las claves de las entidades de resultado en lugar de las entidades en sí, con una latencia y un coste inferiores a los de la recuperación de 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 económico hacer primero una consulta solo de claves y, a continuación, obtener un subconjunto de entidades de los resultados, en lugar de ejecutar una consulta general que puede obtener más entidades de las que realmente necesitas.

Elementos Extent

Una extensión de JDO representa todos los objetos del almacén de datos de una clase concreta. Para crearla, debes pasar la clase que quieras al método getExtent() de PersistenceManager. La interfaz Extent amplía la interfaz Iterable para acceder a los resultados y obtenerlos en lotes según sea necesario. Cuando hayas terminado de acceder a los resultados, llama al método closeAll() de la extensión.

En el siguiente ejemplo se itera sobre todos los objetos Person de Datastore:

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

// ...

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

Eliminar entidades mediante consultas

Si vas a enviar una consulta con el objetivo de eliminar todas las entidades que coincidan con el filtro de consulta, puedes ahorrarte algo de código usando la función "eliminar por consulta" de JDO. Con lo siguiente, se eliminan todas las personas que superen una altura determinada:

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

Verás que la única diferencia es que llamamos a q.deletePersistentAll() en lugar de q.execute(). Todas las reglas y restricciones descritas anteriormente para los filtros, los criterios de ordenación y los índices se aplican a las consultas, tanto si seleccionas como si eliminas el conjunto de resultados. Sin embargo, ten en cuenta que, al igual que si hubieras eliminado estas Person entidades con pm.deletePersistent(), también se eliminarán los elementos secundarios dependientes de las entidades eliminadas por la consulta. Para obtener más información sobre los elementos secundarios dependientes, consulta la página Relaciones de entidades en JDO.

Cursores de consultas

En JDO, puedes usar una extensión y la clase JDOCursorHelper para usar cursores con consultas de JDO. Los cursores funcionan al obtener resultados como una lista o al usar un iterador. Para obtener un cursor, debes transferir 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, consulta la página Consultas de Datastore.

Política de lectura del almacén de datos y fecha límite de llamada

Puedes definir la política de lectura (coherencia fuerte o coherencia final) y el plazo de la llamada a Datastore para todas las llamadas realizadas por una instancia de PersistenceManager mediante la configuración. También puedes anular estas opciones en un objeto Query concreto. Sin embargo, ten en cuenta que no hay forma de anular la configuración de estas opciones cuando obtienes entidades por clave.

Cuando se selecciona la coherencia final para una consulta de Datastore, los índices que usa la consulta para recoger los resultados también se acceden con coherencia final. En ocasiones, las consultas devuelven entidades que no coinciden con los criterios de la consulta, aunque esto también ocurre con una política de lectura de coherencia fuerte. Si la consulta usa un filtro de ancestro, puedes usar transacciones para asegurarte de que el conjunto de resultados sea coherente.

Para anular la política de lectura de 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 indique lo contrario en el archivo de configuración jdoconfig.xml.

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

q.setDatastoreReadTimeoutMillis(3000);

El valor es un intervalo de tiempo expresado en milisegundos.