Requêtes NDB

Une application peut utiliser des requêtes pour rechercher dans le datastore des entités correspondant à des critères de recherche spécifiques, appelés filtres.

Présentation

Une application peut utiliser des requêtes pour rechercher dans le datastore des entités correspondant à des critères de recherche spécifiques, appelés filtres. Par exemple, une application qui effectue le suivi de plusieurs livres d'or pourrait utiliser une requête pour récupérer les messages d'un livre d'or, classés par date :

from google.appengine.ext import ndb
...
class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)
...
class MainPage(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 20

    def get(self):
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*')
        greetings = Greeting.query_book(ancestor_key).fetch(
            self.GREETINGS_PER_PAGE)

        self.response.out.write('<html><body>')

        for greeting in greetings:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write('</body></html>')

Certaines requêtes sont plus complexes que d'autres ; le datastore a besoin d'index intégrés pour ces dernières. Ces index intégrés sont spécifiés dans un fichier de configuration nommé index.yaml. Sur le serveur de développement, si vous exécutez une requête nécessitant un index que vous n'avez pas spécifié, le serveur l'ajoute automatiquement à son fichier index.yaml. Cependant, sur votre site Web, une requête nécessitant un index non encore spécifié échoue. Par conséquent, le cycle de développement type consiste à essayer une nouvelle requête sur le serveur de développement, puis à mettre à jour le site Web de manière à utiliser le fichier index.yaml automatiquement modifié. Vous pouvez mettre à jour le fichier index.yaml indépendamment de l'importation de l'application en exécutant la commande gcloud app deploy index.yaml. Si votre datastore comporte de nombreuses entités, la création d'un index à y associer prend beaucoup de temps. Dans ce cas, il peut être utile de mettre à jour les définitions d'index avant de transférer du code utilisant le nouvel index. Vous pouvez utiliser la console d'administration pour savoir quand la création des index sera terminée.

App Engine Datastore prend en charge de manière native les filtres pour les correspondances exactes (l'opérateur ==) et les comparaisons (les opérateurs <, <=,> et> =). Il prend en charge la combinaison de plusieurs filtres à l'aide d'une opération booléenne AND, avec certaines restrictions (voir ci-dessous).

En plus des opérateurs natifs, l'API accepte l'opérateur !=, la combinaison de groupes de filtres à l'aide de l'opération booléenne OR et l'opération IN, qui teste l'égalité à l'une des valeurs possibles (comme l'opérateur "in" de Python). Ces opérations n'effectuent pas un mappage 1:1 sur les opérations natives du datastore et sont, par conséquent, un peu décalées et relativement lentes. Elles sont mises en œuvre à l'aide de la fusion en mémoire des flux de résultats. Notez que p != v est mis en œuvre sous la forme "p < v OR p > v". (Ceci est important pour les propriétés répétées.)

Restrictions : Le datastore applique certaines restrictions aux requêtes. La violation de ces règles entraîne des exceptions. Par exemple, il est actuellement interdit de combiner trop de filtres, d'utiliser des inégalités pour plusieurs propriétés ou de combiner une inégalité avec un ordre de tri sur une propriété différente. De plus, les filtres faisant référence à plusieurs propriétés nécessitent parfois la configuration d'index secondaires.

Non pris en charge : Le datastore n'accepte pas de correspondances de sous-chaînes, les correspondances non sensibles à la casse ou la recherche dite en texte intégral. Il existe des moyens de mettre en œuvre des correspondances non sensibles à la casse, voire une recherche en texte intégral, à l'aide de propriétés calculées.

Filtrer par valeurs de propriété

Rappelez la classe Account à partir des propriétés NDB :

class Account(ndb.Model):
    username = ndb.StringProperty()
    userid = ndb.IntegerProperty()
    email = ndb.StringProperty()

Généralement, vous ne souhaitez pas récupérer toutes les entités d'un genre donné. Seules celles ayant une valeur spécifique ou une plage de valeurs pour une certaine propriété vous intéresse.

Les objets de propriété surchargent certains opérateurs pour le renvoi des expressions de filtre pouvant être utilisées pour contrôler une requête. Par exemple, pour rechercher toutes les entités de compte dont la propriété userid a exactement la valeur 42, vous pouvez utiliser l'expression :

query = Account.query(Account.userid == 42)

(Si vous êtes sûr qu'il n'y a qu'un Account associé à ce userid, vous préférerez peut-être utiliser userid comme clé. Account.get_by_id(...) est plus rapide que Account.query(...).get().)

NDB est compatible avec les opérations suivantes :

property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])

Pour filtrer sur une inégalité, vous pouvez utiliser la syntaxe suivante :

query = Account.query(Account.userid >= 40)

Cela permet de rechercher toutes les entités Account dont la propriété userid est supérieure ou égale à 40.

Deux de ces opérations, != et IN, sont mises en œuvre comme des combinaisons des autres, et sont un peu bizarres, comme décrit dans Opérations != et IN.

Vous pouvez spécifier plusieurs filtres :

query = Account.query(Account.userid >= 40, Account.userid < 50)

Cela combine les arguments de filtre spécifiés en renvoyant toutes les entités du compte, dont la valeur d'identifiant est supérieure ou égale à 40 et inférieure à 50.

Remarque : Comme indiqué précédemment, le datastore rejette les requêtes en filtrant sur les inégalités d'une propriété.

Au lieu de spécifier un filtre de requête entier dans une seule expression, il peut être plus commode de le créer par étapes. Par exemple :

query1 = Account.query()  # Retrieve all Account entitites
query2 = query1.filter(Account.userid >= 40)  # Filter on userid >= 40
query3 = query2.filter(Account.userid < 50)  # Filter on userid < 50 too

query3 équivaut à la variable query de l'exemple précédent. Notez que les objets de requête étant immuables, la construction de query2 n'affecte pas query1 et celle de query3 n'affecte ni query1, ni query2.

Opérations != et IN

Rappelez la classe Article à partir des propriétés NDB :

class Article(ndb.Model):
    title = ndb.StringProperty()
    stars = ndb.IntegerProperty()
    tags = ndb.StringProperty(repeated=True)

Les opérations != (inégalité) et IN (appartenance) sont mises en œuvre en combinant d'autres filtres utilisant l'opération OR. La première d'entre elles,

property != value

est mise en œuvre sous la forme

(property < value) OR (property > value)

Exemple :

query = Article.query(Article.tags != 'perl')

est équivalent à :

query = Article.query(ndb.OR(Article.tags < 'perl',
                             Article.tags > 'perl'))

Remarque : Contre toute attente, cette requête ne recherche pas les entités Article n'incluant pas "perl" en tant que tag. Elle trouve, au contraire, toutes les entités dont au moins un tag est différent de "perl". Par exemple, l'entité suivante serait incluse dans les résultats, même si l'un de ses tags est "perl" :

Article(title='Perl + Python = Parrot',
        stars=5,
        tags=['python', 'perl'])

Cependant, cet élément ne serait pas inclus :

Article(title='Introduction to Perl',
        stars=3,
        tags=['perl'])

Il n'y a aucun moyen de rechercher des entités n'incluant pas de tag égal à "perl".

De même, l'opération IN

property IN [value1, value2, ...]

qui teste l'appartenance à une liste de valeurs possibles, est mise en œuvre comme suit :

(property == value1) OR (property == value2) OR ...

Exemple :

query = Article.query(Article.tags.IN(['python', 'ruby', 'php']))

est équivalent à :

query = Article.query(ndb.OR(Article.tags == 'python',
                             Article.tags == 'ruby',
                             Article.tags == 'php'))

Remarque : Les requêtes utilisant OR dédupliquent leurs résultats. Le flux de résultats n'inclut pas l'entité plusieurs fois, même si elle correspond à deux sous-requêtes ou plus.

Interroger des propriétés répétées

La classe Article définie dans la section précédente sert également d'exemple pour l'interrogation de propriétés répétées. Un filtre, tel que

utilise une valeur unique, même si Article.tags est une propriété répétée. Vous ne pouvez pas comparer des propriétés répétées aux objets d'une liste (le datastore ne le comprendra pas). Par ailleurs, un filtre comme

Article.tags.IN(['python', 'ruby', 'php'])

ne recherche pas du tout les entités Article dont la valeur des tags correspond à la liste ['python', 'ruby', 'php'], mais recherche les entités dont la valeur tags (considérée comme une liste) contient au moins une de ces valeurs.

L'interrogation d'une valeur None sur une propriété répétée a un comportement indéfini. C'est la raison pour laquelle il est déconseillé d'utiliser cette méthode.

Combiner des opérations AND et OR

Vous pouvez imbriquer des opérations AND et OR de manière arbitraire. Exemple :

query = Article.query(ndb.AND(Article.tags == 'python',
                              ndb.OR(Article.tags.IN(['ruby', 'jruby']),
                                     ndb.AND(Article.tags == 'php',
                                             Article.tags != 'perl'))))

Cependant, en raison de la mise en œuvre de OR, une requête de ce formulaire trop complexe peut échouer en indiquant une exception. Il est plus prudent de normaliser ces filtres afin qu'il y ait (tout au plus) une seule opération OR en haut de l'arborescence des expressions et un seul niveau d'opérations AND en dessous.

Pour effectuer cette normalisation, vous devez vous rappeler à la fois des règles de la logique booléenne et du fonctionnement réel de la mise en œuvre des filtres != et IN :

  1. Développez les opérateurs != et IN selon leur forme primitive, où != permet de vérifier que la propriété est < ou > à la valeur, et IN permet de vérifier que la propriété est == à la première, à la seconde ou à la suivante et ainsi de suite jusqu'à la toute dernière valeur de la liste.
  2. Un opérateur AND comportant un OR est équivalent à un opérateur OR de plusieurs AND appliqués aux opérandes AND d'origine, avec un seul opérande OR substitué au OR d'origine. Par exemple, AND(a, b, OR(c, d)) correspond à OR(AND(a, b, c), AND(a, b, d)).
  3. Un opérateur AND contenant un opérande qui est lui-même une opération AND peut intégrer les opérandes de l'opération AND imbriquée dans l'opérande AND qui l'englobe. Par exemple, AND(a, b, AND(c, d)) correspond à AND(a, b, c, d).
  4. Un opérateur OR contenant un opérande qui est lui-même une opération OR peut intégrer les opérandes de l'opération OR imbriquée dans l'opérande OR qui l'englobe. Par exemple, OR(a, b, OR(c, d)) correspond à OR(a, b, c, d).

Si vous appliquez ces transformations par étapes à l'exemple de filtre, en utilisant une notation plus simple que celle de Python, vous obtenez ce qui suit :

  1. Utilisation de la règle n°1 sur les opérateurs IN et != :
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Utilisation de la règle n°2 sur le OR imbriqué le plus profondément dans un AND :
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Utilisation de la règle n°4 sur le OR imbriqué dans un autre OR :
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Utilisation de la règle n°2 sur le OR restant imbriqué dans un AND :
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', AND(tags == 'php', tags < 'perl')),
       AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
  5. Utilisation de la règle n°3 pour réduire les AND imbriqués restants :
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', tags == 'php', tags < 'perl'),
       AND(tags == 'python', tags == 'php', tags > 'perl'))

Attention : Pour certains filtres, cette normalisation peut provoquer une explosion combinatoire. Examinons le AND de trois clauses OR comportant deux clauses de base chacune. En cas de normalisation, il devient un OR de huit clauses AND comportant trois clauses de base chacune. Autrement dit, on passe de six termes à 24.

Spécifier des ordres de tri

Vous pouvez utiliser la méthode order() pour spécifier l'ordre dans lequel une requête renvoie ses résultats. Cette méthode prend une liste d'arguments, chacun étant soit un objet de propriété (à trier par ordre croissant), soit sa négation (indiquant un ordre décroissant). Exemple :

query = Greeting.query().order(Greeting.content, -Greeting.date)

Toutes les entités Greeting sont récupérées, triées par valeur croissante de leur propriété content. Les exécutions d'entités consécutives ayant la même propriété de contenu sont triées par valeur décroissante de leur propriété date. Vous pouvez utiliser plusieurs appels order() pour obtenir le même effet :

query = Greeting.query().order(Greeting.content).order(-Greeting.date)

Remarque : Lorsque vous combinez des filtres avec order(), Datastore rejette certaines combinaisons. En particulier, lorsque vous utilisez un filtre d'inégalité, le premier ordre de tri (le cas échéant) doit spécifier la même propriété que le filtre. En outre, vous devez parfois configurer un index secondaire.

Requêtes ascendantes

Les requêtes ascendantes permettent d'effectuer des requêtes à forte cohérence vers le datastore. Toutefois, les entités ayant le même ancêtre sont limitées à 1 écriture par seconde. Voici une comparaison simple des compromis et de la structure entre une requête ascendante et une requête non ascendante utilisant des clients et leurs achats associés dans le datastore.

L'exemple de requête non ascendante suivant comporte une entité dans le datastore pour chaque Customer et une entité dans le datastore pour chaque Purchase, avec une KeyProperty pointant vers le client.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    customer = ndb.KeyProperty(kind=Customer)
    price = ndb.IntegerProperty()

Pour rechercher tous les achats appartenant au client, vous pouvez utiliser la requête suivante :

purchases = Purchase.query(
    Purchase.customer == customer_entity.key).fetch()

Dans ce cas, le datastore offre un débit en écriture élevé, mais seulement une cohérence à terme. Si un nouvel achat est ajouté, les données obtenues peuvent être obsolètes. Vous pouvez éliminer ce problème en utilisant des requêtes ascendantes.

Pour les clients et les achats avec des requêtes ascendantes, vous conservez la même structure avec deux entités distinctes. La partie client est la même. Cependant, lorsque vous créez des achats, vous n'avez plus besoin de spécifier KeyProperty() pour les achats. En effet, lorsque vous utilisez des requêtes ascendantes, vous appelez la clé de l'entité client lors de la création d'une entité d'achat.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    price = ndb.IntegerProperty()

Chaque achat comporte une clé, ainsi que le client. Cependant, chaque clé d'achat comportera la clé customer_entity. Notez que cela se limite à une écriture par ancêtre par seconde. Ce qui suit permet de créer une entité avec un ancêtre :

purchase = Purchase(parent=customer_entity.key)

Pour interroger les achats d'un client donné, utilisez la requête suivante.

purchases = Purchase.query(ancestor=customer_entity.key).fetch()

Attributs de requête

Les objets de requête comportent les attributs de données en lecture seule suivants :

Attribut Type Par défautDescription
kindstr None Nom du genre (généralement le nom de la classe)
ancestorKey None Ancêtre spécifié pour la requête
filters FilterNode None Expression de filtre
ordersOrder None Ordres de tri

Le fait d'imprimer un objet de requête (ou d'appeler str() ou repr() sur celui-ci) produit une représentation sous forme de chaîne bien formatée :

print(Employee.query())
# -> Query(kind='Employee')
print(Employee.query(ancestor=ndb.Key(Manager, 1)))
# -> Query(kind='Employee', ancestor=Key('Manager', 1))

Filtrer par valeur de propriété structurée

Une requête peut filtrer directement sur les valeurs de champ de propriétés structurées. Par exemple, une requête pour tous les contacts ayant une adresse dont la ville est 'Amsterdam' ressemblerait à ce qui suit :

query = Contact.query(Contact.addresses.city == 'Amsterdam')

Si vous combinez plusieurs filtres de ce type, ceux-ci peuvent correspondre à différentes sous-entités Address au sein de la même entité Contact. Exemple :

query = Contact.query(Contact.addresses.city == 'Amsterdam',  # Beware!
                      Contact.addresses.street == 'Spear St')

peut trouver des contacts ayant une adresse dont la ville est 'Amsterdam' et une autre adresse dont la rue est 'Spear St'. Toutefois, au moins pour les filtres d'égalité, vous pouvez créer une requête qui renvoie uniquement les résultats contenant plusieurs valeurs dans une même sous-entité :

query = Contact.query(Contact.addresses == Address(city='San Francisco',
                                                   street='Spear St'))

Si vous utilisez cette technique, les propriétés de la sous-entité égales à None sont ignorées dans la requête. Si une propriété a une valeur par défaut, vous devez la définir explicitement sur None pour l'ignorer dans la requête. Dans le cas contraire, la requête inclut un filtre impliquant que la valeur de la propriété soit égale à la valeur par défaut. Par exemple, si le modèle Address avait une propriété country avec default='us', l'exemple ci-dessus ne renverrait que les contacts dont le pays est 'us'. Pour inclure les contacts d'autres pays, vous devez utiliser le filtre Address(city='San Francisco', street='Spear St', country=None).

Si une sous-entité a des valeurs de propriété égales à None, elles sont ignorées. Par conséquent, il est inutile de filtrer sur une valeur de propriété de sous-entité égale à None.

Utiliser des propriétés nommées par chaîne

Parfois, vous souhaitez filtrer ou trier une requête en fonction d'une propriété dont le nom est spécifié par chaîne. Par exemple, si vous laissez l'utilisateur entrer des requêtes de recherche comme tags:python, il serait intéressant de les transformer comme suit :

Article.query(Article."tags" == "python") # does NOT work

Si le modèle est un Expando, alors le filtre peut utiliser GenericProperty, la classe utilisée par le genre Expando pour les propriétés dynamiques :

property_to_query = 'location'
query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF')

L'utilisation de GenericProperty fonctionne également si votre modèle n'est pas un Expando. Toutefois, si vous voulez vous assurer que vous n'utilisez que des noms de propriété définis, vous pouvez également utiliser l'attribut de classe _properties

query = Article.query(Article._properties[keyword] == value)

ou utiliser getattr() pour l'obtenir depuis la classe :

query = Article.query(getattr(Article, keyword) == value)

La différence réside dans le fait que getattr() utilise le "nom Python" de la propriété, tandis que _properties est indexé selon le "nom du datastore" de la propriété. Ceux-ci ne diffèrent que lorsque la propriété a été déclarée comme suit :

class ArticleWithDifferentDatastoreName(ndb.Model):
    title = ndb.StringProperty('t')

Ici, le nom Python est title, mais celui du datastore est t.

Ces approches fonctionnent également pour le tri des résultats de requête :

expando_query = FlexEmployee.query().order(ndb.GenericProperty('location'))

property_query = Article.query().order(Article._properties[keyword])

Itérateurs de requêtes

Lorsqu'une requête est en cours, son état est conservé dans un objet itérateur. (La plupart des applications ne les utilisent pas directement. Il est normalement plus simple d'appeler fetch(20) que de manipuler l'objet itérateur.) Il existe deux méthodes de base pour obtenir un tel objet :

  • Utiliser la fonction iter() intégrée de Python sur un objet Query
  • Appeler la méthode iter() de l'objet Query

La première permet d'utiliser une boucle for Python (qui appelle implicitement la fonction iter()) pour effectuer une boucle sur une requête.

for greeting in greetings:
    self.response.out.write(
        '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

La seconde façon qui consiste à utiliser la méthode iter() de l'objet Query permet de transmettre des options à l'itérateur afin d'affecter son comportement. Par exemple, pour utiliser une requête contenant uniquement des clés dans une boucle for, vous pouvez écrire ceci :

for key in query.iter(keys_only=True):
    print(key)

Les itérateurs de requêtes ont d'autres méthodes utiles :

Méthode Description
__iter__() Fait partie du protocole d'itération de Python.
next() Renvoie le résultat suivant ou déclenche l'exception StopIteration en l'absence de résultat.

has_next() Renvoie True si un appel next() ultérieur renvoie un résultat, False s'il déclenche StopIteration.

Bloque jusqu'à ce que la réponse à cette question soit connue et met en mémoire tampon le résultat (le cas échéant) jusqu'à ce que vous le récupériez avec next().
probably_has_next() Semblable à has_next(), mais utilise un raccourci plus rapide (et parfois inexact).

Peut renvoyer un faux positif (True si next() déclenche réellement StopIteration), mais jamais un faux négatif (False si next() renvoie réellement un résultat).
cursor_before() Renvoie un curseur de requête représentant un point juste avant le renvoi du dernier résultat.

Déclenche une exception si aucun curseur n'est disponible (en particulier, si l'option de requête produce_cursors n'a pas été transmise).
cursor_after() Renvoie un curseur de requête représentant un point juste après le renvoi du dernier résultat.

Déclenche une exception si aucun curseur n'est disponible (en particulier, si l'option de requête produce_cursors n'a pas été transmise).
index_list() Renvoie la liste des index utilisés par une requête exécutée, y compris les index primaires, composites, de types et à propriété unique.

Curseurs de requêtes

Un curseur de requête est une petite structure de données opaque représentant un point de reprise dans une requête. Celui-ci est utile pour montrer à un utilisateur une page de résultats à la fois. Il est également utile pour gérer les travaux longs qui doivent éventuellement être arrêtés et repris. On l'utilise généralement avec la méthode fetch_page() d'une requête. Il fonctionne un peu comme fetch(), à la différence qu'il renvoie un triple (results, cursor, more). L'option more renvoyée indique qu'il y a probablement plus de résultats. Une interface utilisateur peut utiliser ceci, par exemple, pour supprimer un bouton ou un lien "Page suivante". Pour demander les pages suivantes, transmettez le curseur renvoyé par un appel à fetch_page() au suivant. Une erreur BadArgumentError est déclenchée si vous transmettez un curseur non valide. Notez que la validation vérifie uniquement si la valeur est codée en base64. Vous devrez effectuer toute autre validation nécessaire.

Ainsi, pour permettre à l'utilisateur de voir toutes les entités correspondant à une requête, en procédant une page à la fois, votre code pourrait ressembler à ceci :

from google.appengine.datastore.datastore_query import Cursor
...
class List(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 10

    def get(self):
        """Handles requests like /list?cursor=1234567."""
        cursor = Cursor(urlsafe=self.request.get('cursor'))
        greets, next_cursor, more = Greeting.query().fetch_page(
            self.GREETINGS_PER_PAGE, start_cursor=cursor)

        self.response.out.write('<html><body>')

        for greeting in greets:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        if more and next_cursor:
            self.response.out.write('<a href="/list?cursor=%s">More...</a>' %
                                    next_cursor.urlsafe())

        self.response.out.write('</body></html>')

Notez l'utilisation de urlsafe() et Cursor(urlsafe=s) pour sérialiser et désérialiser le curseur. Cela permet de transmettre un curseur à un client sur le Web dans la réponse à une requête et de le recevoir du client ultérieurement.

Remarque : La méthode fetch_page() renvoie généralement un curseur même s'il n'y a plus de résultats, mais cela n'est pas garanti. En effet, la valeur de curseur renvoyée peut être None. Notez également que, dans la mesure où l'option more est mise en œuvre à l'aide de la méthode probably_has_next() de l'itérateur, il se peut que, dans de rares circonstances, elle renvoie True même si la page suivante est vide.

Certaines requêtes NDB ne prennent pas en charge les curseurs de requête, toutefois vous pouvez les réparer. Si une requête utilise IN, OR ou !=, les résultats de la requête ne fonctionnent pas avec les curseurs à moins d'être triés par clé. Si une application ne trie pas les résultats par clé et appelle fetch_page(), elle obtient une erreur BadArgumentError. Si User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N) obtient une erreur, remplacez-le par : User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)

Au lieu de "parcourir" les résultats d'une requête, vous pouvez utiliser la méthode iter() d'une requête pour obtenir un curseur à un point précis. Pour ce faire, transmettez produce_cursors=True à iter(). Lorsque l'itérateur se trouve au bon endroit, appelez sa méthode cursor_after() pour obtenir un curseur situé juste à sa suite. (Ou appelez cursor_before() pour placer un curseur juste avant.) Notez que l'appel à cursor_after() ou cursor_before() peut entraîner un appel bloquant de Datastore à cause de la réexécution d'une partie de la requête afin d'extraire un curseur qui pointe vers le milieu d'un lot.

Pour utiliser un curseur pour parcourir les résultats de la requête dans le sens inverse, créez une requête inverse :

# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
q_reverse = q.order(-Bar.key)

# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)

# Fetch the same page going backward.
r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor)

Appeler une fonction pour chaque entité ("mappage")

Supposons que vous deviez obtenir les entités Account correspondant aux entités Message renvoyées par une requête. Vous pourriez écrire quelque chose de semblable à ceci :

message_account_pairs = []
for message in message_query:
    key = ndb.Key('Account', message.userid)
    account = key.get()
    message_account_pairs.append((message, account))

Cependant, cette méthode est plutôt inefficace, dans la mesure où elle attend de récupérer une entité pour l'utiliser et ainsi de suite. Le temps d'attente est important. Une autre méthode consiste à écrire une fonction de rappel mappée sur les résultats de la requête :

def callback(message):
    key = ndb.Key('Account', message.userid)
    account = key.get()
    return message, account

message_account_pairs = message_query.map(callback)
# Now message_account_pairs is a list of (message, account) tuples.

Cette version fonctionne un peu plus rapidement que la boucle for simple ci-dessus, dans la mesure où elle offre une certaine simultanéité. Cependant, comme l'appel à get() dans callback() est toujours synchrone, le gain n'est pas énorme. C'est à ce moment qu'il convient d'utiliser les requêtes Get asynchrones.

GQL

GQL est un langage de type SQL permettant de récupérer des entités ou des clés depuis App Engine Datastore. Alors que les fonctionnalités de GQL diffèrent de celles d'un langage de requête pour base de données relationnelle classique, la syntaxe GQL est semblable à celle de SQL. La syntaxe GQL est décrite dans la documentation de référence de GQL.

Vous pouvez utiliser GQL pour créer des requêtes. Cette opération s'apparente à la création d'une requête avec Model.query(), mais utilise la syntaxe GQL pour définir le filtre et l'ordre de la requête. Pour l'utiliser :

  • ndb.gql(querystring) renvoie un objet Query (du même type que celui renvoyé par Model.query()). Toutes les méthodes habituelles sont disponibles sur ces objets Query : fetch(), map_async(), filter(), etc.
  • Model.gql(querystring) est une notation abrégée pour ndb.gql("SELECT * FROM Model " + querystring). En règle générale, querystring se présente plus ou moins sous la forme "WHERE prop1 > 0 AND prop2 = TRUE".
  • Pour interroger des modèles contenant des propriétés structurées, vous pouvez utiliser foo.bar dans votre syntaxe GQL pour faire référence à des sous-propriétés.
  • GQL prend en charge les liaisons de paramètres de type SQL. Une application peut définir une requête, puis y associer des valeurs :
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    ou
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    L'appel de la fonction bind() d'une requête a pour effet de renvoyer une nouvelle requête et non de modifier la requête d'origine.

  • Si la classe de modèle remplace la méthode de classe _get_kind(), la requête GQL doit utiliser le genre renvoyé par cette fonction, et non le nom de la classe.
  • Si une propriété du modèle remplace son nom (par exemple, foo = StringProperty('bar')) la requête GQL doit utiliser le nom de la propriété remplacée (dans l'exemple, bar).

Utilisez toujours la fonction de liaison de paramètre si certaines valeurs de votre requête sont des variables fournies par l'utilisateur. Cela évite les attaques syntaxiques.

Rechercher un modèle qui n'a pas été importé (ou, plus généralement, défini) est une erreur.

Utiliser un nom de propriété non défini par la classe de modèle est une erreur, sauf si ce modèle est un Expando.

La spécification d'une limite ou d'un décalage dans la méthode fetch() de la requête ignore la limite ou le décalage défini par les clauses OFFSET et LIMIT de GQL. Ne combinez pas OFFSET et LIMIT avec fetch_page(). Notez que la limite de 1 000 résultats imposée par App Engine sur les requêtes s'applique à la fois au décalage et à la limite.

Si vous êtes habitué à SQL, méfiez-vous des fausses hypothèses lorsque vous utilisez GQL. GQL est traduit dans l'API de requête native NDB. Cela est différent d'un mappeur objet-relationnel classique (comme SQLAlchemy ou le support de base de données Django), dans lequel les appels d'API sont traduits en SQL avant d'être transmis au serveur de base de données. GQL n'est pas compatible avec les modifications du datastore (insertions, suppressions ou mises à jour) et ne prend en charge que les requêtes.