Présentation de la bibliothèque cliente NDB Python 2

La bibliothèque cliente NDB de Google Datastore permet aux applications Python d'App Engine de se connecter à Datastore. La bibliothèque cliente NDB est fondée sur l'ancienne bibliothèque DB Datastore avec les fonctionnalités de datastore supplémentaires suivantes :

  • La classe StructuredProperty, qui permet aux entités d'avoir une structure imbriquée.
  • Mise en cache automatique intégrée, qui assure généralement des lectures rapides et peu coûteuses via un cache contextuel et Memcache.
  • Compatibilité avec les API asynchrones pour les actions simultanées et les API synchrones.

Cette page dresse une introduction et une présentation de la bibliothèque cliente NDB.

Définir des entités, des clés et des propriétés

Datastore stocke des objets de données, appelés entités. Une entité possède une ou plusieurs propriétés, correspondant à des valeurs nommées associées à l'un des différents types de données compatibles. Par exemple, une propriété peut être une chaîne, un nombre entier ou une référence à une autre entité.

Chaque entité est identifiée par une clé, correspondant à un identifiant unique dans le datastore de l'application. La clé peut avoir un parent, autrement dit une autre clé. Ce parent peut avoir lui-même un parent, etc. Au sommet de cette "chaîne" de parents se trouve une clé sans parent, appelée racine.

Affiche la relation entre l'entité racine et les entités enfants dans un groupe d'entités

Les entités dont les clés ont la même racine forment un groupe d'entités ou un groupe. Si des entités appartiennent à des groupes différents, les modifications qui leur sont apportées peuvent parfois sembler se produire "dans le désordre". Si les entités ne sont pas liées dans la sémantique de votre application, ce n'est pas un problème. Toutefois, si les modifications de certaines entités doivent être cohérentes, votre application doit les intégrer au même groupe lors de leur création.

Le schéma de relation entre entités et l'exemple de code ci-dessous montrent comment un Guestbook peut avoir plusieurs Greetings, qui possèdent chacun des propriétés content et date.

Affiche les relations entre entités telles qu'elles ont été créées dans l'exemple de code inclus

Cette relation est mise en œuvre dans l'exemple de code ci-dessous.

import cgi
import textwrap
import urllib

from google.appengine.ext import ndb

import webapp2

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):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

        greeting_blockquotes = []
        for greeting in greetings:
            greeting_blockquotes.append(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write(textwrap.dedent("""\
            <html>
              <body>
                {blockquotes}
                <form action="/sign?{sign}" method="post">
                  <div>
                    <textarea name="content" rows="3" cols="60">
                    </textarea>
                  </div>
                  <div>
                    <input type="submit" value="Sign Guestbook">
                  </div>
                </form>
                <hr>
                <form>
                  Guestbook name:
                    <input value="{guestbook_name}" name="guestbook_name">
                    <input type="submit" value="switch">
                </form>
              </body>
            </html>""").format(
                blockquotes='\n'.join(greeting_blockquotes),
                sign=urllib.urlencode({'guestbook_name': guestbook_name}),
                guestbook_name=cgi.escape(guestbook_name)))

class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()
        self.redirect('/?' + urllib.urlencode(
            {'guestbook_name': guestbook_name}))

app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', SubmitForm)
])

Utiliser des modèles pour stocker des données

Un modèle est une classe qui décrit un type d'entité, y compris les types et la configuration de ses propriétés. Il s'apparente plus ou moins à une table en SQL. Une entité peut être créée en appelant le constructeur de la classe du modèle, puis stockée en appelant la méthode put().

Cet exemple de code définit la classe de modèle Greeting. Chaque entité Greeting possède deux propriétés : le contenu textuel du message de félicitations et la date à laquelle il a été créé.

class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)
class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()

Pour créer et stocker un nouveau message d'accueil, l'application crée un objet Greeting et appelle sa méthode put().

Pour garantir que les messages d'accueil d'un livre d'or n'apparaissent pas "en désordre", l'application définit une clé parent lors de la création d'un objet Greeting. Ainsi, le nouveau message de félicitations appartiendra au même groupe d'entités que d'autres messages de félicitations du même livre d'or. L'application exploite ce principe lors de l'exécution de requêtes : elle utilise une requête ascendante.

Requêtes et index

Une application peut exécuter des requêtes pour rechercher des entités correspondant à certains filtres.

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

Une requête NDB typique filtre les entités par genre. Dans cet exemple, query_book génère une requête qui renvoie des entités Greeting. Une requête peut également spécifier des filtres au niveau des clés et des valeurs des propriétés d'entités. Comme dans cet exemple, une requête peut spécifier un ancêtre, pour ne rechercher que les entités qui "appartiennent à" un ancêtre donné. Elle peut également spécifier un ordre de tri. Une entité est renvoyée en tant que résultat d'une requête si elle possède au moins une valeur (même nulle) pour chacune des propriétés mentionnées dans les filtres et ordres de tri de la requête, et que les valeurs des propriétés répondent à tous les critères de filtrage.

Chaque requête utilise un index, c'est-à-dire une table contenant les résultats de la requête dans l'ordre souhaité. Le datastore sous-jacent gère automatiquement les index simples (ceux qui utilisent une seule propriété).

Il définit ses index complexes dans un fichier de configuration, index.yaml. Le serveur Web de développement ajoute automatiquement des suggestions à ce fichier lorsqu'il rencontre des requêtes pour lesquelles aucun index n'a encore été configuré.

Vous pouvez ajuster des index manuellement en modifiant le fichier avant d'importer l'application. Vous pouvez mettre à jour les index séparément de l'importation de l'application en exécutant 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.

Ce mécanisme d'indexation est compatible avec un vaste éventail de requêtes et convient à la plupart des applications. Cependant, il n'accepte pas certains types de requêtes courants dans d'autres technologies de base de données, notamment les jointures.

Comprendre les écritures NDB : valider, invalider le cache et appliquer

NDB écrit les données par étapes :

  • Lors de la phase de commit, le service Datastore sous-jacent enregistre les modifications.
  • NDB invalide ses caches pour la ou les entités affectées. Ainsi, les futures lectures seront effectuées (et mises en cache) dans le datastore sous-jacent au lieu de lire les valeurs obsolètes du cache.
  • Enfin, peut-être quelques secondes plus tard, le datastore sous-jacent applique la modification. Cela rend la modification visible pour les requêtes globales et pour des lectures cohérentes à terme.

La fonction NDB qui écrit les données (par exemple, put()) répond après l'invalidation du cache, tandis que la phase d'application se déroule de manière asynchrone.

Des nouvelles tentatives sont exécutées automatiquement en cas d'échec pendant la phase de commit, mais si les échecs persistent, votre application reçoit une exception. Si la phase de commit aboutit, mais que la phase d’application échoue, cette dernière est déployée jusqu'à son terme lorsque l’une des situations suivantes se présente :

  • Des "balayages" périodiques effectués par Datastore contrôlent les tâches de commit inachevées et les appliquent.
  • La prochaine écriture, transaction ou lecture à cohérence forte dans le groupe d'entités affecté entraîne, avant la lecture, l'écriture ou la transaction, l'application des modifications qui n'ont pas encore été prises en compte.

Ce comportement affecte la façon et le moment auquel les données sont visibles pour votre application. La modification peut ne pas être complètement appliquée dans le datastore sous-jacent quelques centaines de millisecondes environ après la réponse de la fonction NDB. Une requête non ascendante effectuée lors de l'application d'une modification peut afficher un état incohérent, c'est-à-dire une partie mais pas la totalité de la modification.

Transactions et mise en cache de données

La bibliothèque cliente NDB peut regrouper plusieurs opérations dans une seule transaction. La transaction ne peut réussir que si toutes les opérations qu'elle contient aboutissent. Si l'une des opérations échoue, un rollback de la transaction est effectué automatiquement. Ceci est particulièrement utile pour les applications Web distribuées, où plusieurs utilisateurs peuvent accéder aux mêmes données ou les manipuler en même temps.

NDB utilise Memcache en tant que service de cache pour les "points chauds" dans les données. Si l'application lit souvent certaines entités, NDB peut les lire rapidement à partir du cache.

Utiliser Django avec NDB

Pour utiliser NDB avec le framework Web Django, ajoutez google.appengine.ext.ndb.django_middleware.NdbDjangoMiddleware à l'entrée MIDDLEWARE_CLASSES dans votre fichier Django settings.py. Il est préférable de l'insérer devant toute autre classe de middleware, car un autre middleware peut effectuer des appels au datastore qui ne pourront pas être gérés correctement si ce middleware en question est appelé avant Django. Pour en savoir plus, consultez la page sur le middleware Django.

Étapes suivantes

Apprenez-en davantage sur :