Modélisation des données en Python

Remarque : Les développeurs qui créent des applications sont vivement encouragés à utiliser la bibliothèque cliente NDB qui présente plusieurs avantages supplémentaires par rapport à cette bibliothèque cliente, tels que la mise en cache automatique des entités via l'API Memcache. Si vous utilisez actuellement l'ancienne bibliothèque cliente DB, consultez le guide de migration de DB vers NDB.

Présentation

Une entité Datastore contient une clé et un ensemble de propriétés. Une application utilise l'API Datastore pour définir des modèles de données, ainsi que pour créer des instances de ces modèles et les stocker sous forme d'entités. Les modèles fournissent une structure commune pour les entités créées par l'API et permettent de définir des règles pour valider les valeurs des propriétés.

Classes de modèles

La classe Model

Une application décrit les types de données qu'elle utilise à l'aide de modèles. Un modèle est une classe Python qui hérite de la classe Model. La classe de modèle définit un nouveau genre d'entité Datastore et les propriétés prévues pour ce genre. Le nom du genre est défini par le nom de la classe instanciée qui hérite de db.Model.

Les propriétés de modèle sont définies à l'aide d'attributs de classe. Chaque attribut de classe est une instance d'une sous-classe de la classe Property, généralement l'une des classes de propriété fournies. Une instance de propriété contient la configuration de la propriété. Celle-ci indique, par exemple, si la propriété est obligatoire pour que l'instance soit correcte, ou une valeur par défaut à utiliser pour l'instance, si aucune valeur n'est fournie.

from google.appengine.ext import db

class Pet(db.Model):
    name = db.StringProperty(required=True)
    type = db.StringProperty(required=True, choices=set(["cat", "dog", "bird"]))
    birthdate = db.DateProperty()
    weight_in_pounds = db.IntegerProperty()
    spayed_or_neutered = db.BooleanProperty()

Une entité appartenant à l'un des genres d'entités définis est représentée dans l'API par une instance de la classe de modèle correspondante. Pour créer une nouvelle entité, l'application appelle le constructeur de la classe. Les attributs de l'instance permettent à l'application d'accéder aux propriétés de l'entité et de les manipuler. Le constructeur d'instances de modèle accepte les valeurs initiales des propriétés en tant qu'arguments de mot clé.

from google.appengine.api import users

pet = Pet(name="Fluffy",
          type="cat")
pet.weight_in_pounds = 24

Remarque : Les attributs de la classe de modèle correspondent à la configuration des propriétés du modèle, dont les valeurs sont des instances Property. Les attributs de l'instance de modèle sont les valeurs réelles des propriétés, dont le type est accepté par la classe Property.

La classe Model utilise les instances Property pour valider les valeurs affectées aux attributs de l'instance de modèle. La validation des valeurs des propriétés a lieu lorsque vous construisez une instance de modèle et que vous affectez une nouvelle valeur à un attribut d'instance. Cette méthode permet de garantir que la valeur affectée à une propriété est toujours correcte.

Dans la mesure où la validation se produit lors de la construction de l'instance, toute propriété configurée comme obligatoire doit être initialisée dans le constructeur. Dans cet exemple, name et type sont des valeurs obligatoires. Par conséquent, leurs valeurs initiales sont spécifiées dans le constructeur. weight_in_pounds n'est pas requis par le modèle. Ainsi, ce n'est qu'ultérieurement qu'une valeur lui est attribuée.

Une instance d'un modèle créé à l'aide du constructeur n'existe pas dans le datastore tant qu'elle n'y est pas "mise" pour la première fois.

Remarque : Comme pour tous les attributs de classe Python, la configuration des propriétés de modèle est initialisée après la première importation du script ou du module. Vous pouvez initialiser la configuration du module pendant une requête d'un utilisateur et la réutiliser pendant une requête d'un autre utilisateur, car App Engine met en cache les modules importés entre les requêtes. Veillez à ne pas initialiser la configuration des propriétés de modèle, par exemple les valeurs par défaut, avec des données propres à la requête ou à l'utilisateur actuel. Pour en savoir plus, consultez la section Mise en cache des applications.

La classe Expando

Un modèle défini à l'aide de la classe Model établit un ensemble fixe de propriétés que chaque instance de la classe doit contenir (avec des valeurs par défaut, éventuellement). Bien que cette méthode facilite la modélisation des objets de données, toutes les entités d'un genre donné dans le datastore ne doivent pas obligatoirement contenir le même ensemble de propriétés.

Parfois, il est utile qu'une entité contienne des propriétés qui soient différentes de celles des autres entités du même genre. Une telle entité est représentée dans l'API Datastore par un modèle Expando. Une classe de modèle Expando correspond à une sous-classe de la super-classe Expando. Toute valeur affectée à un attribut d'une instance d'un modèle Expando devient une propriété de l'entité Datastore, utilisant le nom de l'attribut. Ces propriétés sont appelées propriétés dynamiques. Les propriétés définies à l'aide d'instances de classe Property dans des attributs de classe sont des propriétés fixes.

Un modèle Expando peut contenir des propriétés fixes et des propriétés dynamiques. La classe Model définit simplement des attributs de classe avec des objets de configuration Property pour les propriétés fixes. L'application crée les propriétés dynamiques lorsqu'elle leur attribue des valeurs.

class Person(db.Expando):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    hobbies = db.StringListProperty()

p = Person(first_name="Albert", last_name="Johnson")
p.hobbies = ["chess", "travel"]

p.chess_elo_rating = 1350

p.travel_countries_visited = ["Spain", "Italy", "USA", "Brazil"]
p.travel_trip_count = 13

Les propriétés dynamiques ne sont pas validées, car elles ne disposent pas de définitions de propriétés de modèle. Toute propriété dynamique peut avoir la valeur de n'importe quel type de base de datastore, y compris None. Deux entités de genre identique peuvent contenir différents types de valeurs pour la même propriété dynamique, et une propriété non définie dans une entité peut être définie dans l'autre entité.

Contrairement aux propriétés fixes, les propriétés dynamiques n'ont pas besoin d'exister. Une propriété dynamique avec la valeur None est différente d'une propriété dynamique inexistante. Si une instance de modèle Expando ne contient pas d'attribut pour une propriété, l'entité de données correspondante ne contient pas cette propriété. Pour supprimer une propriété dynamique, supprimez l'attribut.

Les attributs dont le nom commence par un trait de soulignement (_) ne sont pas enregistrés dans l'entité Datastore. Ainsi, vous pouvez stocker dans l'instance de modèle des valeurs destinées à un usage interne temporaire, sans que cela n'affecte les données enregistrées avec l'entité.

Remarque : Les propriétés statiques sont toujours enregistrées dans l'entité Datastore, qu'elles soient associées à une classe Expando ou Model, et même si elles commencent par un trait de soulignement (_).

del p.chess_elo_rating

Une requête qui utilise une propriété dynamique dans un filtre renvoie uniquement les entités dans lesquelles la valeur de la propriété est du même type que la valeur utilisée dans la requête. De façon similaire, la requête renvoie uniquement les entités dans lesquelles cette propriété est définie.

p1 = Person()
p1.favorite = 42
p1.put()

p2 = Person()
p2.favorite = "blue"
p2.put()

p3 = Person()
p3.put()

people = db.GqlQuery("SELECT * FROM Person WHERE favorite < :1", 50)
# people has p1, but not p2 or p3

people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1", 50)
# people has no results

Remarque : l'exemple ci-dessus utilise des requêtes de plusieurs groupes d'entités, qui peuvent renvoyer des résultats obsolètes. Pour obtenir des résultats cohérents, utilisez des requêtes ascendantes dans des groupes d'entités.

La classe Expando est une sous-classe de la classe Model, et elle hérite de toutes ses méthodes.

La classe PolyModel

L'API Python comprend également une classe de modélisation des données qui permet de définir des arborescences de classes et d'effectuer des requêtes susceptibles de renvoyer des entités d'une classe donnée ou de l'une de ses sous-classes. Ces modèles et requêtes sont qualifiés de "polymorphes", car ils permettent aux instances d'une classe d'être les résultats d'une requête d'une classe parente.

L'exemple suivant définit une classe Contact associée aux sous-classes Person et Company :

from google.appengine.ext import db
from google.appengine.ext.db import polymodel

class Contact(polymodel.PolyModel):
    phone_number = db.PhoneNumberProperty()
    address = db.PostalAddressProperty()

class Person(Contact):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    mobile_number = db.PhoneNumberProperty()

class Company(Contact):
    name = db.StringProperty()
    fax_number = db.PhoneNumberProperty()

Ce modèle permet de garantir que toutes les entités Person et toutes les entités Company contiennent des propriétés phone_number et address, et que les requêtes sur les entités Contact peuvent renvoyer les entités Person ou Company. Seules les entités Person contiennent des propriétés mobile_number.

Les sous-classes peuvent être instanciées comme n'importe quelle autre classe de modèle :

p = Person(phone_number='1-206-555-9234',
           address='123 First Ave., Seattle, WA, 98101',
           first_name='Alfred',
           last_name='Smith',
           mobile_number='1-206-555-0117')
p.put()

c = Company(phone_number='1-503-555-9123',
            address='P.O. Box 98765, Salem, OR, 97301',
            name='Data Solutions, LLC',
            fax_number='1-503-555-6622')
c.put()

Une requête sur les entités Contact peut renvoyer des instances de Contact, Person ou Company. Le code suivant permet d'imprimer des informations pour les deux entités créées ci-dessus :

for contact in Contact.all():
    print 'Phone: %s\nAddress: %s\n\n' % (contact.phone_number,
                                          contact.address)

Une requête sur les entités Company renvoie uniquement des instances de Company :

for company in Company.all()
    # ...

Pour le moment, les modèles polymorphes ne doivent pas être transmis directement au constructeur de la classe Query. Utilisez plutôt la méthode all(), comme illustré dans l'exemple ci-dessus.

Pour en savoir plus sur l'utilisation des modèles polymorphes et leur mise en œuvre, consultez la page Classe PolyModel.

Types et classes de propriétés

Le datastore prend en charge un ensemble fixe de types de valeurs pour les propriétés des entités, y compris les chaînes Unicode, les entiers, les nombres à virgule flottante, les dates, les clés d'entités, les chaînes d'octets (blobs), ainsi que divers types GData. Chacun des types de valeurs du datastore correspond à une classe Property fournie par le module google.appengine.ext.db.

La page Types et classes de propriétés décrit tous les types de valeur pris en charge et les classes de propriétés correspondantes. Vous trouverez ci-dessous la description de plusieurs types de valeurs particuliers.

Chaînes et blobs

Le datastore prend en charge deux types de valeur pour le stockage de texte : les chaînes de texte courtes d'une longueur maximale de 1 500 octets et les chaînes de texte longues d'une longueur maximale de 1 Mo. Les chaînes courtes sont indexées et peuvent s'utiliser dans les critères de filtre des requêtes et les ordres de tri. Les chaînes longues ne sont pas indexées et ne peuvent pas être utilisées dans les critères de filtre ou les ordres de tri.

Une valeur de chaîne courte peut correspondre à une valeur unicode ou à une valeur str. Si la valeur est de type str, le codage par défaut est en 'ascii'. Pour indiquer un codage différent pour une valeur str, vous pouvez la convertir en une valeur unicode à l'aide du constructeur du type unicode(), qui utilise le str et le nom du codage comme arguments. Les chaînes courtes peuvent être modélisées à l'aide de la classe StringProperty.

class MyModel(db.Model):
    string = db.StringProperty()

obj = MyModel()

# Python Unicode literal syntax fully describes characters in a text string.
obj.string = u"kittens"

# unicode() converts a byte string to a Unicode string using the named codec.
obj.string = unicode("kittens", "latin-1")

# A byte string is assumed to be text encoded as ASCII (the 'ascii' codec).
obj.string = "kittens"

# Short string properties can be used in query filters.
results = db.GqlQuery("SELECT * FROM MyModel WHERE string = :1", u"kittens")

Une valeur de chaîne longue est représentée par une instance db.Text. Son constructeur utilise soit une valeur unicode, soit une valeur str accompagnée, éventuellement, du nom du codage utilisé dans la valeur str. Les chaînes longues peuvent être modélisées à l'aide de la classe TextProperty.

class MyModel(db.Model):
    text = db.TextProperty()

obj = MyModel()

# Text() can take a Unicode string.
obj.text = u"lots of kittens"

# Text() can take a byte string and the name of an encoding.
obj.text = db.Text("lots of kittens", "latin-1")

# If no encoding is specified, a byte string is assumed to be ASCII text.
obj.text = "lots of kittens"

# Text properties can store large values.
obj.text = db.Text(open("a_tale_of_two_cities.txt").read(), "utf-8")

Le datastore accepte également deux types similaires pour les chaînes d'octets non textuelles  : db.ByteString et db.Blob. Ces valeurs sont des chaînes d'octets bruts et ne sont pas traitées comme du texte encodé (tel que UTF-8).

À l'instar des valeurs db.StringProperty, les valeurs db.ByteString sont indexées. Comme pour les propriétés db.TextProperty, les valeurs db.ByteString sont limitées à 1 500 octets. Une instance ByteString représente une chaîne d'octets courte et utilise une valeur str comme argument pour son constructeur. Les chaînes d'octets sont modélisées avec la classe ByteStringProperty.

Comme dans le cas de db.Text, une valeur db.Blob peut atteindre un méga-octet, mais elle n'est pas indexée et ne peut pas être utilisée dans des filtres de requête ou des ordres de tri. La classe db.Blob utilise une valeur str comme argument par défaut pour son constructeur, mais vous pouvez affecter la valeur directement. Les blobs sont modélisés à l'aide de la classe BlobProperty.

class MyModel(db.Model):
    blob = db.BlobProperty()

obj = MyModel()

obj.blob = open("image.png").read()

Listes

Une propriété peut prendre plusieurs valeurs, représentées dans l'API Datastore sous forme de list Python. La liste peut contenir des valeurs appartenant à n'importe quel type pris en charge par le datastore. Une seule propriété de liste peut même prendre des valeurs de différents types.

L'ordre est généralement préservé. Ainsi, lorsque les entités sont renvoyées par des requêtes et par get (), les valeurs des propriétés de la liste sont dans le même ordre que lors de leur stockage. Seule exception à cette règle : les valeurs Blob et Text sont placées en fin de liste, mais conservent leur ordre initial l'une par rapport à l'autre.

La classe ListProperty modélise une liste et impose que toutes les valeurs de la liste soient d'un type donné. Pour plus de commodité, la bibliothèque fournit également la classe StringListProperty, semblable à ListProperty(basestring).

class MyModel(db.Model):
    numbers = db.ListProperty(long)

obj = MyModel()
obj.numbers = [2, 4, 6, 8, 10]

obj.numbers = ["hello"]  # ERROR: MyModel.numbers must be a list of longs.

Une requête avec filtres sur une propriété de liste teste individuellement chaque valeur dans la liste. L'entité correspond à la requête uniquement si une valeur de la liste passe tous les filtres sur cette propriété. Pour plus d'informations, consultez la page Requêtes Datastore.

# Get all entities where numbers contains a 6.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers = 6")

# Get all entities where numbers contains at least one element less than 10.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers < 10")

Les filtres de requête fonctionnent uniquement sur les membres de la liste. Il est impossible de tester la similarité de deux listes dans un filtre de requête.

En interne, le datastore représente la valeur d'une propriété de liste sous forme de valeurs multiples pour la propriété. Si la valeur d'une propriété de liste est la liste vide, la propriété n'est pas représentée dans le datastore. L'API Datastore traite cette situation de manière distincte pour les propriétés statiques (avec ListProperty) et les propriétés dynamiques :

  • Vous pouvez affecter à une propriété statique ListProperty une valeur correspondant à la liste vide. La propriété n'existe pas dans le datastore, mais l'instance de modèle se comporte comme si la valeur était la liste vide. Une propriété statique ListProperty ne peut pas prendre la valeur None.
  • Une propriété dynamique avec la valeur list ne peut pas se voir attribuer une valeur de liste vide. Toutefois, vous pouvez lui affecter une valeur de None, et la supprimer (à l'aide de del).

Le modèle ListProperty vérifie que le type d'une valeur ajoutée à la liste est correct, et envoie un message BadValueError si ce n'est pas le cas. Ce test s'effectue (et échoue éventuellement) même lorsqu'une entité stockée précédemment est récupérée et importée dans le modèle. Dans la mesure où les valeurs str sont converties en valeurs unicode (texte ASCII) avant leur stockage, ListProperty(str) est traitée comme ListProperty(basestring), le type de données Python qui accepte à la fois des valeurs str et unicode. Vous pouvez également utiliser StringListProperty() à cette fin.

Pour stocker des chaînes d'octets non textuelles, utilisez les valeurs db.Blob. Les octets d'une chaîne blob sont conservés lors de leur stockage et de leur récupération. Vous pouvez déclarer une propriété correspondant à une liste d'objets blob en tant que ListProperty(db.Blob).

Les propriétés de liste peuvent interagir avec les ordres de tri de manière inhabituelle. Pour en savoir plus, consultez la page Requêtes Datastore.

Références

Une valeur de propriété peut contenir la clé d'une autre entité. La valeur est une instance Key.

La classe ReferenceProperty modélise une valeur de clé et impose que toutes les valeurs se rapportent à des entités d'un type donné. Pour plus de commodité, la bibliothèque fournit également SelfReferenceProperty, une propriété équivalente à ReferenceProperty, mais qui fait référence au même type que l'entité avec la propriété.

Lors de l'affectation d'une instance de modèle à une propriété ReferenceProperty, sa clé est automatiquement utilisée comme valeur.

class FirstModel(db.Model):
    prop = db.IntegerProperty()

class SecondModel(db.Model):
    reference = db.ReferenceProperty(FirstModel)

obj1 = FirstModel()
obj1.prop = 42
obj1.put()

obj2 = SecondModel()

# A reference value is the key of another entity.
obj2.reference = obj1.key()

# Assigning a model instance to a property uses the entity's key as the value.
obj2.reference = obj1
obj2.put()

Vous pouvez utiliser une valeur de propriété ReferenceProperty comme s'il s'agissait de l'instance de modèle de l'entité référencée. Si l'entité référencée ne figure pas dans la mémoire, l'utilisation de la propriété comme instance permet de récupérer automatiquement l'entité dans le datastore. La propriété ReferenceProperty stocke également une clé, mais l'utilisation de cette propriété déclenche le chargement de l'entité associée.

obj2.reference.prop = 999
obj2.reference.put()

results = db.GqlQuery("SELECT * FROM SecondModel")
another_obj = results.fetch(1)[0]
v = another_obj.reference.prop

Lorsqu'une clé pointe vers une entité inexistante, l'accès à la propriété génère une erreur. Si une application s'attend à ce qu'une référence puisse être incorrecte, elle peut vérifier l'existence de l'objet à l'aide d'un bloc try/except :

try:
  obj1 = obj2.reference
except db.ReferencePropertyResolveError:
  # Referenced entity was deleted or never existed.

La classe ReferenceProperty dispose d'une autre fonctionnalité pratique : les références arrière. Lorsqu'un modèle a une propriété ReferenceProperty vers un autre modèle, chaque entité référencée dispose d'une propriété dont la valeur est une requête qui renvoie toutes les entités du premier modèle qui y fait référence.

# To fetch and iterate over every SecondModel entity that refers to the
# FirstModel instance obj1:
for obj in obj1.secondmodel_set:
    # ...

Par défaut, le nom de la propriété de référence arrière est modelname_set (avec le nom de la classe de modèle en minuscules et "_set" ajouté à la fin). Il est possible de l'ajuster en utilisant l'argument collection_name pour le constructeur ReferenceProperty.

Si plusieurs valeurs ReferenceProperty font référence à la même classe de modèle, la construction par défaut de la propriété de référence arrière génère une erreur :

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class raises a DuplicatePropertyError with the message
# "Class Firstmodel already has property secondmodel_set"
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel)
    reference_two = db.ReferenceProperty(FirstModel)

Pour éviter cette erreur, vous devez définir explicitement l'argument collection_name :

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class runs fine
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_one_set")
    reference_two = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_two_set")

Le référencement et le déréférencement automatiques des instances de modèle, la vérification du type et les références arrière ne sont disponibles que lorsque vous utilisez la classe de propriété de modèle ReferenceProperty. Les clés stockées sous forme de valeurs de propriétés dynamiques Expando ou de valeurs ListProperty ne disposent pas de ces fonctionnalités.