Consultas do Datastore na JDO

Este documento tem como foco o uso da biblioteca de persistência Java Data Objects (JDO) (em inglês) para consultas do App Engine Datastore. Para ver informações gerais sobre consultas, consulte a página principal Consultas do Datastore.

Uma consulta recupera entidades do Datastore que atendem a um conjunto específico de condições. A consulta opera em entidades de um determinado tipo. Ela pode especificar filtros nos valores de propriedades, chaves e ancestrais das entidades e pode retornar zero ou mais entidades como resultados. Também especifica ordens de classificação para colocar os resultados em sequência pelos respectivos valores de propriedade. Os resultados incluem todas as entidades que tenham pelo menos um valor (que pode ser nulo) para cada propriedade nomeada nos filtros e ordens de classificação e que também tenham valores de propriedade que atendam a todos os critérios de filtro especificados. A consulta pode retornar entidades de projeção, inteiras ou apenas chaves de entidade.

Uma consulta típica inclui:

Quando executada, a consulta recupera todas as entidades do tipo determinado que satisfaçam a todos os filtros fornecidos, classificadas na ordem especificada. As consultas são executadas no modo de somente leitura.

Observação: para economizar memória e melhorar o desempenho, uma consulta precisa, sempre que possível, especificar um limite para o número de resultados retornados.

Observação: o mecanismo de consulta baseado em índice é compatível com uma grande variedade de consultas e adequado à maioria dos aplicativos. No entanto, ele não aceita alguns tipos de consulta comuns em outras tecnologias de banco de dados. Especificamente, mesclagens e consultas agregadas não são compatíveis com o mecanismo de consulta do Datastore. Para conhecer as limitações das consultas do Datastore, consulte a página Consultas do Datastore.

Consultas com JDOQL

A JDO inclui uma linguagem de consulta para recuperar objetos que atendem a um conjunto de critérios. Essa linguagem, chamada JDOQL, refere-se diretamente às classes e campos de dados JDO, e inclui a verificação de tipos para parâmetros e resultados de consultas. A JDOQL é semelhante à SQL, mas é mais adequada para bancos de dados orientados a objetos, como o App Engine Datastore. A implementação da API JDO do App Engine não é compatível com consultas SQL diretamente.

A interface Query da JDO é compatível com vários estilos de chamada: você pode especificar uma consulta completa em uma string usando a sintaxe da string JDOQL ou especificar algumas ou todas as partes da consulta chamando métodos no objeto Query. Veja no exemplo a seguir o estilo de método de chamada, com um filtro e uma ordem de classificação, usando a substituição de parâmetros para o valor utilizado no filtro. Os valores de argumento transmitidos ao método execute() do objeto Query são substituídos na consulta na ordem especificada:

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 é a mesma consulta, usando a sintaxe da 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");

Você pode misturar os estilos de definição da consulta. Exemplo:

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

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

É possível reutilizar uma única instância Query com valores diferentes substituídos pelos parâmetros chamando o método execute() várias vezes. Cada chamada realizará a consulta e retornará os resultados como um conjunto.

A sintaxe de string da JDOQL é compatível com a especificação literal de valores numéricos e de string. Todos os outros tipos de valor precisam usar a substituição de parâmetros. Literais na string de consulta podem ser colocados entre aspas simples (') ou duplas ("). Veja aqui um exemplo que usa uma string literal:

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

Filtros

Um filtro de propriedade especifica:

  • um nome de propriedade;
  • um operador de comparação;
  • um valor de propriedade.
Exemplo:

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

O aplicativo precisa fornecer o valor da propriedade. Ele não pode referir-se a outras propriedades nem ser calculado em relação a elas. Uma entidade satisfaz ao filtro se tiver uma propriedade com o nome determinado com valor igual ao especificado no filtro, da maneira descrita pelo operador de comparação.

O operador de comparação pode ser qualquer um destes:

Operador Significado
== Igual a
< Menor que
<= Menor que ou igual a
> Maior que
>= Maior que ou igual a
!= Diferente de

Conforme descrito na página principal Consultas, uma única consulta não pode usar filtros de desigualdade (<, <=, >, >=, !=) em mais de uma propriedade. Vários filtros de desigualdade na mesma propriedade, como consultar um intervalo de valores, são permitidos. Os filtros contains(), correspondentes a filtros IN no SQL, são compatíveis com a seguinte sintaxe:

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

O operador "diferente de" (!=) realiza duas consultas: uma em que todos os outros filtros permanecem inalterados e o filtro "diferente de" é substituído por um filtro menor que (<) e outro em que é substituído por um filtro "maior que" (>). Em seguida, os resultados são mesclados na ordem. Uma consulta não pode ter mais que um filtro "diferente de", e a que tiver um não poderá ter outros filtros de desigualdade.

O operador contains() também executa várias consultas: uma para cada item na lista especificada, com todos os outros filtros inalterados e o filtro contains() substituído por um filtro de igualdade (===). Os resultados são mesclados na ordem dos itens na lista. Se uma consulta tiver mais que um filtro contains(), ela será executada como várias consultas, uma para cada combinação possível de valores nas listas contains().

Uma única consulta contendo os operadores diferentes de != ou contains() é limitada a no máximo 30 subconsultas.

Saiba mais sobre como as consultas != e contains() são convertidas em várias consultas em uma estrutura JDO/JPA no artigo Consultas com filtros != e IN.

Na sintaxe de string da JDOQL, você pode separar vários filtros com os operadores && ("e" lógico) e || ("ou" lógico):

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

O operador de negação ("NOT" lógico) não é compatível. Lembre-se também de que o operador || só pode ser usado quando os filtros que separa têm o mesmo nome de propriedade, ou seja, quando podem ser combinados em um único 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'");

Ordens de classificação

A ordem de classificação de uma consulta especifica:

  • um nome de propriedade;
  • uma direção de classificação (crescente ou decrescente).

Por exemplo:

// 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 exemplo:

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

Se uma consulta incluir várias ordens de classificação, elas serão aplicadas na sequência especificada. O exemplo a seguir classifica primeiro pelo sobrenome crescente e, em seguida, pela altura decrescente:

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

Se nenhuma ordem de classificação for especificada, os resultados serão retornados na ordem em que forem recuperados do Datastore.

Observação: devido à maneira como o Datastore executa consultas, se uma consulta especificar filtros de desigualdade em uma propriedade e ordens de classificação em outras propriedades, a propriedade usada nos filtros de desigualdade precisará ser ordenada antes das outras propriedades.

Intervalos

Uma consulta pode especificar um intervalo de resultados a serem retornados para o aplicativo. O intervalo indica quais resultados no conjunto completo precisam ser o primeiro e último retornados. Os resultados são identificados por seus índices numéricos. 0 indica o primeiro resultado no conjunto. Por exemplo, um intervalo de 5,, 10 retorna os resultados do 6º ao 10º:

q.setRange(5, 10);

Observação: o uso de intervalos pode afetar o desempenho, porque o Datastore precisa recuperar e depois descartar todos os resultados que precedem o deslocamento inicial. Por exemplo, uma consulta com o intervalo 5,, 10 recupera dez resultados do Datastore, descarta os cinco primeiros e retorna os cinco restantes para o aplicativo.

Consultas com base em chaves

As chaves de entidade podem ser o assunto de um filtro de consulta ou de uma ordem de classificação. O Datastore considera a chave-valor completa para essas consultas, incluindo o caminho dos ancestrais da entidade, o tipo e a string do nome da chave atribuída pelo aplicativo ou o ID numérico atribuído pelo sistema. Como a chave é exclusiva em todas as entidades no sistema, as consultas de chave facilitam a recuperação de entidades de um determinado tipo em lotes, como para um despejo de lote do conteúdo do Datastore. Ao contrário dos intervalos JDOQL, isso funciona de maneira eficaz em qualquer número de entidades.

Na comparação por desigualdade, as chaves são ordenadas pelos seguintes critérios, nesta ordem:

  1. Caminho do ancestral
  2. Tipo de entidade
  3. Identificador (nome da chave ou ID numérico)

Os elementos do caminho ancestral são comparados de forma análoga: por tipo (string) e, em seguida, por nome de chave ou ID numérico. Os tipos e nomes de chave são strings e estão ordenados por valor de bytes. Os IDs numéricos são representados em números inteiros e ordenados numericamente. Se entidades com o mesmo pai e do mesmo tipo usarem uma combinação de strings de nomes de chave e IDs numéricos, aquelas com códigos numéricos precederão as com nomes de chave.

Na JDO, use o campo de chave principal do objeto para referir-se à chave de entidade na consulta. Para usar uma chave como um filtro de consulta, especifique o tipo de parâmetro Key para o método declareParameters(). Veja a seguir todas as entidades Person com uma determinada comida favorita, supondo uma relação de um para um não proprietário entre Person e 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());

Uma consulta apenas de chaves retorna somente as chaves das entidades resultantes em vez das entidades inteiras, o que resulta em latência e custo menores:

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

Muitas vezes, é mais econômico fazer uma consulta apenas de chaves primeiro e, em seguida, buscar um subconjunto de entidades a partir dos resultados em vez de executar uma consulta geral, que pode buscar mais entidades do que você realmente precisa.

Extensões

Uma extensão da JDO representa todos os objetos de uma determinada classe no Datastore. Para criar essa extensão, transmita a classe desejada ao método getExtent() do Persistence Manager. A interface Extent estende a interface Iterable para acessar os resultados, recuperando-os em lotes conforme necessário. Quando terminar de acessar os resultados, chame o método closeAll() da extensão.

O exemplo a seguir itera em cada objeto Person no Datastore:

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

// ...

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

Como excluir entidades por consulta

Se você estiver emitindo uma consulta com o objetivo de excluir todas as entidades que correspondem ao filtro, use o recurso "excluir por consulta" da JDO para economizar um pouco de codificação. No exemplo a seguir, todas as pessoas acima de uma determinada altura são excluídas:

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

Você perceberá que a única diferença aqui é que estamos chamando q.deletePersistentAll() em vez de q.execute(). Todas as regras e restrições descritas acima para filtros, ordens de classificação e índices se aplicam a consultas, tanto para a seleção quanto para a exclusão do conjunto de resultados. No entanto, como se você tivesse excluído essas entidades Person com pm.deletePersistent(), todos os filhos dependentes das entidades excluídas pela consulta também serão excluídos. Para saber mais sobre filhos dependentes, veja a página Relacionamentos de entidades na JDO.

Cursores de consulta

Na JDO, você pode usar uma extensão e a classe JDOCursorHelper para usar cursores com consultas JDO. Os cursores funcionam ao buscar resultados como uma lista ou usando um iterador. Para receber um cursor, transmita a lista de resultados ou o iterador para o 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 saber mais sobre cursores de consulta, veja a página Consultas do Datastore.

Política de leitura e duração máxima da chamada do Datastore

Você pode definir a política de leitura (consistência forte x consistência posterior) e o duração máxima de chamada ao Datastore para todas as chamadas feitas por uma instância PersistenceManager usando a configuração. Você também pode substituir essas opções para um objeto Query individual. Observe, no entanto, que não é possível substituir a configuração dessas opções ao buscar entidades por chave.

Quando a consistência eventual é selecionada para uma consulta do Datastore, os índices usados pela consulta para coletar resultados também são acessados com consistência eventual. De vez em quando, as consultas retornam entidades que não correspondem aos critérios, embora isso também ocorra com uma política de leitura fortemente consistente. Se a consulta usar um filtro ancestral, use transações para garantir um conjunto de resultados consistente.

Para modificar a política de leitura de uma única consulta, chame o método addExtension():

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

Os valores possíveis são "EVENTUAL" e "STRONG". o padrão é "STRONG", a menos que definido de outra forma no arquivo de configuração jdoconfig.xml.

Para substituir a duração máxima da chamada ao Datastore por uma única consulta, chame o método setDatastoreReadTimeoutMillis():

q.setDatastoreReadTimeoutMillis(3000);

O valor é um intervalo de tempo expresso em milissegundos.