Requêtes Datastore dans JDO

Ce document aborde l'utilisation du framework de persistance JDO (Java Data Objects) pour les requêtes App Engine Datastore. Pour obtenir des informations plus générales sur les requêtes, consultez la page principale Requêtes Datastore.

Une requête récupère depuis Datastore des entités qui répondent à un ensemble déterminé de conditions. La requête s'exécute sur des entités d'un genre donné. Elle peut spécifier des filtres sur les valeurs de propriété, les clés et les ancêtres des entités, et peut renvoyer zéro, une ou plusieurs entités en tant que résultats. Une requête peut également spécifier des ordres de tri pour séquencer les résultats en fonction de leurs valeurs de propriété. Les résultats incluent toutes les entités qui ont au moins une valeur (pouvant être nulle) pour chaque propriété nommée dans les filtres et les ordres de tri, et dont les valeurs de propriété répondent à tous les critères de filtre spécifiés. La requête peut renvoyer des entités entières, des entités projetées ou simplement des clés d'entité.

Une requête type comprend les éléments suivants :

  • Un genre d'entité auquel s'applique la requête
  • Zéro, un ou plusieurs filtres basés sur les valeurs de propriété, les clés et les ancêtres des entités
  • Zéro, un ou plusieurs ordres de tri, pour séquencer les résultats
Lorsque la requête est exécutée, elle récupère toutes les entités d'un genre donné qui répondent à tous les filtres définis, en les triant dans l'ordre spécifié. Les requêtes s'exécutent en lecture seule.

Remarque : Pour économiser de la mémoire et améliorer les performances, une requête doit, dans la mesure du possible, spécifier une limite concernant le nombre de résultats renvoyés.

Remarque : Le mécanisme de requête basé sur les index permet l'exécution d'un large éventail de requêtes et convient à la plupart des applications. Toutefois, il n'est pas compatible avec certains genres de requêtes couramment rencontrés dans d'autres technologies de base de données. Plus précisément, les requêtes de jointure et d'agrégation ne sont pas acceptées dans le moteur de requêtes en mode Datastore. Pour connaître les limites relatives aux requêtes Datastore, consultez la page Requêtes Datastore.

Requêtes avec JDOQL

JDO intègre un langage de requête permettant de récupérer les objets qui répondent à un ensemble de critères. Ce langage, appelé JDOQL, fait directement référence aux champs et classes de données JDO, et donne lieu à une vérification du type pour les paramètres et résultats des requêtes. JDOQL est semblable à SQL, mais s'avère mieux adapté aux bases de données orientées objet telles que App Engine Datastore. (La mise en œuvre de l'API JDO par App Engine ne prend pas directement en charge les requêtes SQL.)

L'interface Query (requête) de JDO accepte plusieurs styles d'appel : vous pouvez spécifier une requête complète dans une chaîne (à l'aide de la syntaxe de chaîne JDOQL) ou vous pouvez définir une partie ou l'intégralité de la requête en appelant des méthodes sur l'objet Query. L'exemple suivant illustre la méthode consistant à effectuer un appel avec un filtre et un ordre de tri, en appliquant la substitution de paramètres à la valeur utilisée dans le filtre. Les valeurs d'argument transmises à la méthode execute() de l'objet Query sont substituées dans la requête dans l'ordre spécifié :

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

Voici la même requête utilisant la syntaxe de chaîne :

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

Vous pouvez combiner ces deux styles de définition d'une requête. Exemple :

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

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

Vous pouvez réutiliser une même instance de Query avec différentes valeurs substituées pour les paramètres, en appelant la méthode execute() à plusieurs reprises. Chaque appel exécute la requête et renvoie les résultats sous la forme d'une collection.

La syntaxe de chaîne JDOQL permet la spécification littérale des valeurs de chaîne et numériques. Tous les autres types de valeur doivent utiliser la substitution de paramètres. Les littéraux de la chaîne de requête peuvent être placés entre guillemets simples (') ou doubles ("). Voici un exemple utilisant un littéral de chaîne :

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

Filtres

Un filtre de propriété spécifie les éléments suivants :

  • Un nom de propriété
  • Un opérateur de comparaison
  • Une valeur de propriété
Par exemple :

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 valeur de propriété doit être fournie par l'application. Elle ne peut pas faire référence à d'autres propriétés ni être calculée en fonction de celles-ci. Une entité satisfait aux critères de filtre si elle possède une propriété du nom donné, dont la valeur est comparable à celle spécifiée dans le filtre, de la manière décrite par l'opérateur de comparaison.

L'opérateur de comparaison peut être l'un des suivants :

Opérateur Signification
== Égal à
< Inférieur à
<= Inférieur ou égal à
> Supérieur à
>= Supérieur ou égal à
!= Différent de

Comme indiqué sur la page principale Requêtes, une seule requête ne peut pas utiliser de filtres d'inégalité (<, <=, >, >=, !=) sur plusieurs propriétés. L'utilisation de plusieurs filtres d'inégalité sur la même propriété (par exemple, dans le cadre d'une requête portant sur une plage de valeurs) est autorisée. Les filtres contains(), correspondant aux filtres IN en SQL, sont compatibles avec la syntaxe suivante :

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

L'opérateur "différent de" (!=) exécute deux requêtes : une dans laquelle tous les autres filtres restent inchangés et le filtre "différent de" est remplacé par un filtre "inférieur à" (<), puis une autre où il est remplacé par un filtre "supérieur à" (>). Les résultats sont ensuite fusionnés dans l'ordre. Une requête ne doit pas comporter plus d'un filtre "différent de" qui, lorsqu'il est présent, ne peut coexister avec aucun autre filtre d'inégalité.

L'opérateur contains() exécute également plusieurs requêtes : une pour chaque élément de la liste spécifiée, tous les autres filtres restant inchangés et le filtre contains() étant remplacé par un filtre d'égalité (==). Les résultats sont fusionnés dans l'ordre des éléments de la liste. Une requête comportant plus d'un filtre contains() est exécutée sous la forme de plusieurs requêtes, une pour chaque combinaison possible de valeurs dans les listes contains().

Une requête unique contenant des opérateurs "différent de" (!=) ou contains() est limitée à 30 sous-requêtes.

Pour en savoir plus sur la façon dont les requêtes != et contains() se traduisent en plusieurs requêtes dans un framework JDO/JPA, consultez l'article Queries with != and IN filters (Requêtes avec les filtres != et IN).

Dans la syntaxe de chaîne JDOQL, vous pouvez séparer plusieurs filtres avec les opérateurs && ("et" logique) et || ("ou" logique) :

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

La négation (logique "non") n'est pas acceptée. N'oubliez pas non plus que l'opérateur || ne peut être utilisé que lorsque les filtres qu'il sépare ont tous le même nom de propriété (c'est-à-dire, lorsqu'ils peuvent être combinés dans un même filtre 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'");

Ordres de tri

L'ordre de tri d'une requête spécifie les éléments suivants :

  • Un nom de propriété
  • Un sens de tri (croissant ou décroissant)

Exemple :

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

Exemple :

// 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 une requête comprend plusieurs ordres de tri, ceux-ci sont appliqués selon la séquence spécifiée. L'exemple suivant effectue d'abord un tri par ordre croissant de nom, puis par ordre décroissant de taille :

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 aucun ordre de tri n'est spécifié, les résultats sont renvoyés dans l'ordre dans lequel ils sont récupérés depuis Datastore.

Remarque : En raison de la manière dont Datastore exécute les requêtes, si une requête spécifie des filtres d'inégalité sur une propriété et des ordres de tri sur d'autres propriétés, la propriété employée dans les filtres d'inégalité doit être triée avant les autres.

Plages

Une requête peut spécifier une plage de résultats à renvoyer à l'application. La plage indique quels résultats de l'ensemble de résultats doivent être renvoyés en premier et en dernier lieu. Les résultats sont identifiés par leurs index numériques, où 0 désigne le premier résultat de l'ensemble. Par exemple, une plage de 5, 10 renvoie les résultats compris entre le 6e et le 10e inclus :

q.setRange(5, 10);

Remarque : L'utilisation de plages peut affecter les performances, car Datastore doit extraire puis rejeter tous les résultats précédant le décalage de départ. Par exemple, une requête comprenant une plage de 5, 10 extrait dix résultats de Datastore, supprime les cinq premiers, puis renvoie les cinq restants à l'application.

Requêtes basées sur des clés

Les clés d'entité peuvent faire l'objet d'un filtre de requête ou d'un ordre de tri. Datastore prend en compte la valeur de clé complète pour ces requêtes, y compris le chemin d'ancêtre de l'entité, le genre, la chaîne de nom de clé attribuée par l'application ou l'ID numérique attribué par le système. Comme la clé est unique pour toutes les entités du système, les requêtes de clé facilitent la récupération des entités d'un genre donné par lots, par exemple pour un vidage par lot du contenu de Datastore. Contrairement aux plages JDOQL, cette technique fonctionne efficacement pour n'importe quel nombre d'entités.

En cas de comparaison d'inégalité, les clés sont triées selon les critères suivants, dans cet ordre :

  1. Chemin d'ancêtre
  2. Genre d'entité
  3. Identifiant (nom de clé ou ID numérique)

Les éléments du chemin d'ancêtre sont comparés de la même manière : par genre (chaîne), puis par nom de clé ou ID numérique. Les genres et les noms de clé sont des chaînes, et sont triés par valeur d'octet. Les ID numériques sont des entiers et sont triés par ordre numérique. Si des entités ayant le même parent et le même genre emploient une combinaison de chaînes de nom de clé et d'ID numériques, celles avec des ID numériques précèdent celles portant des noms de clé.

Dans JDO, vous utilisez le champ de clé primaire de l'objet pour faire référence à la clé d'entité dans la requête. Pour utiliser une clé en tant que filtre de requête, spécifiez le type de paramètre Key dans la méthode declareParameters(). L'exemple suivant trouve toutes les entités Person ayant un aliment favori donné, en supposant un rapport de un à un entre Person et 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());

Une requête ne contenant que des clés affiche uniquement les clés des entités de résultat, et non les entités elles-mêmes. Cela entraîne une latence et un coût inférieurs à ceux induits par la récupération d'entités entières :

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

Il est souvent plus économique de commencer par ce type de requête, puis de récupérer un sous-ensemble d'entités parmi les résultats, plutôt que d'exécuter une requête générale pouvant récupérer plus d'entités que nécessaire.

Interfaces Extent

Une interface Extent (extension) de JDO représente chaque objet d'une classe spécifique contenu dans Datastore. Pour la créer, transmettez la classe souhaitée à la méthode getExtent() de Persistence Manager. L'interface Extent étend l'interface Iterable pour accéder aux résultats, en les récupérant par lots si nécessaire. Lorsque vous avez terminé de consulter les résultats, appelez la méthode closeAll() de l'extension.

L'exemple suivant parcourt tous les objets Person contenus dans Datastore :

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

// ...

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

Supprimer des entités par requête

Si vous émettez une requête dans le but de supprimer toutes les entités correspondant au filtre de requête, vous pouvez économiser un peu de code grâce à la fonction "delete by query" (supprimer par requête) de JDO. La requête ci-dessous permet de supprimer toutes les personnes d'une taille donnée :

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

Vous remarquerez que la seule différence est que nous appelons q.deletePersistentAll() au lieu de q.execute(). Toutes les règles et restrictions décrites ci-dessus concernant les filtres, les ordres de tri et les index sont valables également pour les requêtes, que vous sélectionniez ou supprimiez l'ensemble de résultats. Notez cependant que, tout comme si vous aviez supprimé ces entités Person avec pm.deletePersistent(), tous les enfants dépendants des entités supprimées par la requête seront également supprimés. Pour en savoir plus sur les enfants dépendants, consultez la page Relations entre entités dans JDO.

Curseurs de requêtes

Dans JDO, vous pouvez utiliser une extension et la classe JDOCursorHelper pour utiliser des curseurs avec les requêtes JDO. Les curseurs fonctionnent dans le cadre de la récupération de résultats sous la forme d'une liste ou de l'utilisation d'un itérateur. Pour obtenir un curseur, transmettez la liste de résultats ou l'itérateur à la méthode statique 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

Pour en savoir plus sur les curseurs de requête, consultez la page Requêtes de Datastore.

Règles de lecture et durée maximale de l'appel au Datastore

Vous pouvez définir les règles de lecture (cohérence forte ou à terme) et la durée maximale de l'appel à Datastore pour tous les appels effectués par une instance PersistenceManager à l'aide de la configuration. Vous pouvez également remplacer ces options par un objet Query individuel. (Notez cependant qu'il n'est pas possible d'ignorer la configuration de ces options lorsque vous extrayez des entités par clé.)

Lorsque la cohérence à terme est sélectionnée pour une requête Datastore, elle s'applique également aux index utilisés par cette requête pour rassembler les résultats. Les requêtes renvoient parfois des entités qui ne correspondent pas aux critères de requête, même si cela est également le cas des règles de lecture à cohérence forte. (Si la requête a recours à un filtre d'ancêtre, vous pouvez utiliser des transactions pour garantir la cohérence de l'ensemble de résultats.)

Pour remplacer les règles de lecture d'une seule requête, appelez sa méthode addExtension() :

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

Les valeurs possibles sont "EVENTUAL" et "STRONG". La valeur par défaut est "STRONG", sauf indication contraire dans le fichier de configuration jdoconfig.xml.

Pour ignorer la durée maximale de l'appel à Datastore pour une seule requête, appelez sa méthode setDatastoreReadTimeoutMillis() :

q.setDatastoreReadTimeoutMillis(3000);

La valeur est un intervalle de temps exprimé en millisecondes.