L’ORM et les modèles Django
Principes et généralités
L’ORM est un acronyme anglais signifiant Object-Relational Mapping. Sa traduction pourrait être : correspondance entre le modèle objet et le modèle relationnel, ce qui tel quel n’est pas très explicite. On parle souvent en français de mapping objet-relationnel. L’ORM correspond à une technique informatique permettant d’établir une correspondance entre les objets manipulés dans un langage de programmation orienté objet et les structures d’une base de données relationnelle (tables, index, requêtes…), donnant ainsi l’impression pour le programmeur d’une base de données orientée objet.
La problématique
Les données utilisées dans un contexte objet sont tout sauf de simples tableaux, mais peuvent être de nature très diverse, et ce encore plus en Python. Sans tout lister, citons : des listes d’objets, des dictionnaires d’objets, des arbres d’objets, etc. et ce bien sûr sans même parler du polymorphisme éventuel des objets Python.
A contrario, une base de données manipule un ensemble d’« objets » simples et de natures (ou types) bien définis, c’est-à-dire des tables ayant un nombre de champs fixes, organisées en colonnes dont le type est prédéterminé parmi une liste bien établie et non extensible : entiers, flottants, chaînes, dates, etc.
On parle souvent de données scalaires pour les bases de données et de données non scalaires pour la programmation orientée objet.
Toute la difficulté d’un ORM réside dans la traduction de la logique objet sous une forme d’éléments élémentaires, lesquels pourront être facilement stockés...
Intérêt de l’ORM
Il existe un réel débat qui alimente les forums Internet sur l’utilité ou non des ORM. Sans rentrer dans celui-ci essayons de regarder quelques points incontestables.
Quelques critiques souvent mentionnées concernant les ORM
Une couche supplémentaire, comme toute automatisation, est une perte de maîtrise et donc une perte de finesse.
Un ORM masque les échanges avec la base, donc le programmeur perd le contrôle direct sur les requêtes SQL envoyées par l’ORM au SGBD.
Bien sûr certains ORM permettent un paramétrage fin pour optimiser les requêtes à la base de données, mais souvent avec une charge de travail additionnelle finalement plus importante que celle de créer les requêtes directement.
Les requêtes générées par un ORM sont parfaites pour les actions de type « CRUD » (Create, Read, Update, Delete), elles peuvent être moins efficaces pour des requêtes de type SELECT, surtout si l’on a des requêtes complexes avec des jointures.
Il est possible de comparer ce débat à celui de savoir s’il faut programmer en assembleur ou en langage évolué. Ce débat en matière de langages a été tranché il y a longtemps : on utilise les deux, mais cela dépend de ce que l’on a à faire. On réserve l’assembleur à des tâches...
Les modèles
Pour Django, les modèles (Models) sont les seuls et uniques points d’accès à vos informations, la seule source de données. Ils contiennent tous les champs de vos tables et décrivent le comportement des objets que vous stockez. D’une façon générale, chaque modèle est associé à une seule table dans la base de données, ce à la différence de certains ORM.
Les principes de base :
-
Chaque modèle est une classe Python dérivée de la classe django.db.models.Model.
-
Chaque attribut de la classe model est représenté par une colonne dans une table de la base de données.
Grâce à cela, Django va pouvoir automatiser tous les accès à la base.
Prenons l’exemple suivant d’un modèle décrivant un employé, caractérisé par son nom et son prénom :
from django.db import models
class Employe(models.Model):
nom = models.CharField(max_length=25)
prenom = models.CharField(max_length=25)
Définitions
Nom et prénom sont des champs du modèle (fields). Ces champs sont traduits sous la forme d’attributs (attributes) de la classe Python et seront associés à une colonne d’une table de la base de données.
Ainsi, la création de ce modèle sera traduite par Django pour la base de données par la commande SQL suivante :
CREATE TABLE application_employe (
"id" int(10) unsigned primary KEY AUTO_INCREMENT,
"nom" varchar(25) NOT NULL,
"prenom" varchar(25) NOT NULL
);
Sur cet exemple, l’ordre de création est écrit pour MySQL mais Django adapte la syntaxe de cet ordre de création à chaque type de base de données qu’il supporte.
Si le nom de la table est généré de manière automatique, à partir du nom du modèle et de l’application qui l’a définie, Django fournit la possibilité de contrôler cette information à travers la manipulation des metadatas (métadonnées - voir metadatas ci-après).
Un champ id est automatiquement ajouté par Django ; là encore Django fournit...
L’héritage entre modèles
Bien que fonctionnant sur la base de l’héritage Python, l’héritage des modèles Django a cependant quelques spécificités. Ces spécificités permettent de contrôler la façon dont le stockage des modèles dérivés sera réalisé dans la base de données. Ont-ils leur propre table SQL ? Partagent-ils celle du modèle dont ils héritent, etc. De ce fait l’héritage présente des contraintes supplémentaires et la liberté totale que donne Python en matière d’héritage se trouve un peu entravée par les limites inhérentes à la gestion par Django de la persistance des objets dans la base.
Pour Django, il y a trois types d’héritages possibles :
-
Les classes abstraites. La classe abstraite mère est uniquement un schéma auquel aucune table de la base de données ne sera associée.
-
L’héritage multitable, permettant de surclasser un modèle existant en lui associant une nouvelle table dans la base de données.
-
Les modèles « proxies » qui permettent de modifier le comportement d’un modèle Django sans toucher aux champs de celui-ci.
1. Les classes abstraites
Le but des classes abstraites est de définir un patron contenant des données et des méthodes communes qui seront partagées par les classes dérivées.
Exemple :
Imaginons que nous réalisions un système de gestion documentaire, permettant de gérer divers types de documents (texte, PDF, images...). Tous les documents présents dans le système ont des attributs/champs en commun. Dans la base nous souhaitons avoir une table par type de document.
Le code basé sur une classe abstraite pourrait ressembler à cela :
from django.db import models
class Document(models.Model):
class Meta:
abstract = True
slug = models.SlugField()
nom_long = models.CharField(max_length=1024)
date_de_publication = models.DateTimeField()
# ...
def __unicode__(self):
return self.slug
class Article(Document) : ...
Liste complète des champs et leurs paramètres
1. Les champs
AutoField
class AutoField(**options)
C’est l’équivalent d’un champ de type entier avec incrémentation automatique dans une base de données SQL.
Pas de gadget de saisie.
BigIntegerField
class BigIntegerField([**options])
Un entier sur 64 bits, permettant de stocker des valeurs de -9223372036854775808 à 9223372036854775807.
Le gadget de saisie associé à ce champ est TextInput.
BinaryField
class BinaryField([**options])
Un champ pour stocker des données brutes en binaire. Il possède des fonctionnalités limitées, telles que le fait de ne pas pouvoir être utilisé comme clef ou filtre.
N’abusez pas de ce champ qui n’est pas fait par exemple pour stocker des fichiers, le stockage statique restant de loin la méthode la plus efficace pour cela. Les bases de données ne sont pas conçues pour le stockage de fichiers.
BooleanField
class BooleanField(**options)
Un champ pour stocker des valeurs booléennes vrai/faux, True/False de Python.
Le gadget de saisie par défaut associé à ce champ est CheckboxInput.
Si vous souhaitez pouvoir utiliser en plus de True/False la valeur None, utilisez plutôt NullBooleanField.
CharField
class CharField(max_length=None[, **options])
Un champ pour stocker des chaînes de caractères. Si vous devez stocker de très grandes chaînes de caractère (du texte), utilisez plutôt TextField.
Le gadget de saisie associé à ce champ est TextInput.
Ce champ a un argument : CharField.max_length. Il permet de définir la taille maximale de cette variable pour le stockage dans la base de données. Cette longueur sera vérifiée par Django.
Attention : certaines bases de données possèdent des contraintes en matière de taille maximale d’un enregistrement.
DateField
class DateField([auto_now=False, auto_now_add=False, **options])
Une instance datetime.date au sens de Python.
Arguments :
(Option) DateField.auto_now : positionne automatiquement la valeur à la date courante, chaque fois que l’on enregistre l’objet. Très pratique pour des jetons de modification, dernière date de login, etc.
(Option) DateField.auto_now_add : positionne automatiquement la valeur à...
Écrire vos propres types de champs
Si dans Django vous ne trouvez pas votre bonheur avec les types de champs prédéfinis (CharField, etc.), vous pouvez écrire vos propres types de champs.
Pourquoi faire cela ? D’une part Django ne couvre que les types les plus courants d’une base de données : entiers, chaînes ou flottants. Ainsi on peut avoir besoin d’écrire le gestionnaire d’un type de champ dans le cas d’un type très spécifique d’une base de données particulière, par exemple des coordonnées géographiques, des objets géométriques, etc. D’autre part, on peut également avoir des champs qui représentent des objets Python complexes dont on souhaite pouvoir gérer de manière fine leur représentation dans la base de données.
Un exemple simple pour gérer un champ de type BLOB (Binary Large Object) dans MySQL, dont nous verrons les différentes options plus en détail dans ce chapitre :
DATABASE_ENGINE = 'django.db.backends.mysql'
class BlobField(models.Field):
__metaclass__ = models.SubfieldBase
def db_type(self):
if DATABASE_ENGINE == 'django.db.backends.mysql':
return 'LONGBLOB'
elif DATABASE_ENGINE ==
'django.db.backends.postgresql_psycopg2':
return 'bytea'
elif DATABASE_ENGINE == 'django.db.backends.sqlite3':
return 'BLOB'
else:
raise NotImplementedError
def to_python(self, value):
if DATABASE_ENGINE ==
'django.db.backends.postgresql_psycopg2':
if value is None:
print value
return value
return str(value)
else:
...
Les options/Meta
Les options globales pour le modèle sont définies dans une classe imbriquée apellée Meta.
Exemple :
class Technicien(Employe):
class Meta:
proxy = True
ordering = ["specialite"]
app_label
Options.app_label
Cette option est à utiliser si un modèle n’est pas situé dans un emplacement ’standard’ ; les emplacements standards sont le fichier models.py ou un package Python nommé models dans l’application.
app_label = 'mon_application'
Dans les versions futures, au-delà de Django 1.7, cette option ne sera plus nécessaire.
db_table
Options.db_table
Cette option permet de préciser le nom de la table dans la base de données à utiliser pour ce modèle ; elle peut être utilisée pour des bases/tables existantes, par exemple : db_table = ’ma_table’.
Si le nom de votre table est un mot clef SQL, ou qu’il contient des caractères interdits en Python, cela est possible car Django protège les noms de tables par des guillemets SQL.
Par défaut, Django crée le nom de la table à partir du nom du modèle en minuscules et de celui de l’application à laquelle le modèle appartient, séparés par un souligné (underscore). Le nom de l’application est le nom du package Python contenant l’application (nom du répertoire de base). Ainsi, si votre application s’appelle compta et le modèle facture alors la table s’appellera compta_facture.
Attention : vous devez préférentiellement utiliser des noms en minuscules pour MySQL.
Oracle possédant une limite de 30 caractères pour le nom des tables, Django pourra donc être amené à tronquer vos noms de tables à 30 et à les passer en majuscules. Si vous souhaitez éviter cela, précédez votre nom de table par des doubles quotes, mais attention cela ne fonctionnera pas avec Oracle : db_table = ’"nom_en_minuscules"’
db_tablespace
Options.db_tablespace
En utilisant les tablespaces, un administrateur peut contrôler les emplacements sur le disque des tables et des index. Django permet grâce à l’option...
Les managers
class Manager(...)
Un manager est une interface gérant les opérations en relation avec la base de données pour les modèles Django ; c’est l’API de l’ORM. Chaque modèle doit avoir au moins un manager. Par défaut, Django crée systématiquement un manager pour chaque modèle en lui donnant le nom objects.
Le comportement de la classe Manager est expliqué à la section Les QuerySet : effectuer des requêtes de ce même chapitre. Nous allons seulement ici décrire comment implémenter des comportements particuliers ou spécifiques à l’aide d’un manager.
Comment renommer le manager par défaut, si on souhaite utiliser le nom objects, par exemple pour un attribut ? Cela est possible de la façon suivante :
from django.db import models
class JavaClass(models.Model):
objects = models.CharField()
classes = models.Manager() # Affectation d'un manager
Dans cet exemple, la syntaxe habituelle JavaClass.objects.all() générera une exception de type AttributeError. Pour récupérer tous les objets on devra écrire JavaClass.classes.all().
Si on n’avait pas défini objects = models.CharField(), le manager objects aurait quand même provoqué une erreur, car lorsque Django trouve un manager, il n’installe plus le manager par défaut.
1. Comment et pourquoi écrire vos propres managers ?
Pour utiliser un manager spécifique dans votre modèle, il suffit d’étendre la classe de base Manager et d’instancier le manager comme un attribut de votre modèle.
class MonModele(models.Model):
mon_manager = MonManager()
# .. .
Il existe deux raisons principales pour lesquelles on peut souhaiter créer un manager spécifique : la première est que l’on souhaite enrichir le manager en lui ajoutant des méthodes additionnelles, la deuxième est que l’on souhaite modifier les QuerySet initiaux retournés par le manager de base.
2. Ajout de méthodes additionnelles
L’ajout de méthodes additionnelles est la façon la plus simple d’ajouter des fonctionnalités de niveau ’table’. C’est-à-dire...
Les QuerySet : effectuer des requêtes
Une fois les modèles de données créés, Django met à votre disposition via l’ORM une API permettant de créer, mettre à jour, lire, rechercher, ou détruire les objets.
from django.db import models
class Formation(models.Model):
nom = models.CharField(max_length=100)
description = models.TextField()
# def __str__(self): # avec Python 3
def __unicode__(self): # avec Python 2
return self.nom
class Auditeur(models.Model):
nom = models.CharField(max_length=50)
prenom = models.CharField(max_length=50)
# def __str__(self): # avec Python 3
def __unicode__(self): # avec Python 2
return ', '.join((self.nom,self.prenom))
class Cours(smodels.Model):
formation = models.ForeignKey(Formation)
auditeurs = models.ManyToManyField(Auditeur)
description = models.TextField()
date = models.DateField()
# def __str__(self): # avec Python 3
def __unicode__(self): # avec Python 2
return self.description
1. Créer des objets
Les objets Python sont représentés dans la base de la façon suivante : un modèle est représenté par une table et chaque instance de ce modèle est représentée par une entrée (une ligne) dans la table.
Pour créer un objet, créez simplement une nouvelle instance à partir des arguments de son modèle, puis utilisez save() pour l’enregistrer.
En dehors de créer les tables et d’en gérer les requêtes, un modèle Django crée automatiquement l’ensemble des arguments nécessaires à la création d’une instance.
Exemple :
>>> from formation.models import Formation
>>> f = Formation(nom='Django', description='Tout ce que vous
voulez savoir sur Django.')
>>> f.save()...
ORM divers
1. Gérer plusieurs bases de données
La quasi-totalité des exemples dans ce livre partent du principe que l’on utilise une base de données unique. Vous avez cependant vu dans ce chapitre apparaître l’attribut ou le paramètre using, permettant de spécifier une base de données alternative. Nous allons voir comment l’utiliser.
a. Première étape : définir plusieurs bases de données
Ouvrez le fichier settings.py et allez à la section « DATABASES= ». La variable DATABASES est un dictionnaire dont les clefs sont les noms utilisables dans Django pour spécifier une base de données. Le nom « default » est important car c’est la base que Django utilise par défaut si l’on ne lui précise pas quelle base utiliser.
Prenons un exemple pour lequel nous avons déclaré deux bases différentes et pas de base par défaut.
DATABASES = {
'default': {},
'users': {
'NAME': 'base_1',
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'USER': 'XXXXXXX',
'PASSWORD': 'XXXXX'
},
'users': {
'NAME': 'base_2',
'ENGINE': 'django.db.backends.mysql',
'USER': 'YYYYYYYYY',
'PASSWORD': 'YYYYYY'
}
}
Nous avons fait exprès de ne pas spécifier de base de données par défaut, il faudra donc toujours spécifier quelle base on utilise. Par ailleurs comme on ne peut pas supprimer la base de données ’default’, pour dire que l’on veut la désactiver, il suffit de lui passer un dictionnaire vide.
Lorsque vous essayez d’accéder à une base de données non définie dans DATABASES, Django lève une exception de type django.db.utils.ConnectionDoesNot Exist.
Commandes syncdb...
Faire évoluer les modèles au cours du développement
Jusqu’à sa version 1.7, Django permettait uniquement de créer les tables de la base de données à partir des modèles, par la commande syncdb. Mais si par la suite, vos modèles évoluaient, ce qui est souvent le cas lors d’un développement, Django ne touchait plus aux tables déjà créées et vous deviez faire évoluer votre base de données manuellement en parallèle de l’évolution de vos modèles. Lorsque l’on regarde la façon dont Django facilite la vie des développeurs sur bien des points, cette absence de synchronisation entre les modèles et la base était un véritable caillou dans la chaussure, qui sans rendre les choses impossibles, les rendait souvent pénibles.
Heureusement, les développeurs Python et Django sont créatifs et ont développé des applications externes pour gérer les migrations, c’est-à-dire la synchronisation entre les modèles et la Base.
L’application la plus populaire et certainement la plus aboutie est South. Citons également django-evolution, car bien que plus basique, cette application règle le problème « faire évoluer les modèles au cours du développement sans trop se préoccuper des données », qui à ce stade n’ont pas encore une grande importance. A contrario, South sait à peu près tout faire : faire évoluer la structure des modèles, faire évoluer les données en fonction de l’évolution des champs (il faut simplement lui dire comment), générer des données initiales, etc.
L’équipe de développement Django a décidé d’intégrer une gestion des migrations à la version1.7.
Avec la version 1.7, la commande syncdb qui se bornait à créer les tables est déclarée obsolète et est remplacée par migrate.
Un système de gestion des migrations est l’équivalent d’un système comme GIT ou SVN, mais pour la structure d’une base de données.
1. Django migrate
Django va stocker dans son système de migration tous les changements...
Django et SQLAlchemy
Comme il n’est pas possible de présenter tous les ORM possibles et envisageables, nous allons uniquement évoquer SQLAlchemy. Cela pour deux raisons : d’une part SQLAlchemy est considéré comme le meilleur ORM Python et, d’autre part, il existe quelques tentatives permettant de marier SQLAlchemy avec Django.
Pour faire simple, SQLAlchemy est un ’toolkit’ Python fait pour manipuler des bases de données avec plus de contrôle sur les manipulations que l’on effectue et également plus de fonctionnalités. L’ORM de Django est parfaitement adapté à Django et au développement web, il permet de modéliser rapidement nos données. C’est une distinction importante : l’interface de l’ORM Django est centrée sur le modèle de données de l’application, tandis que l’interface de SQLAlchemy se concentre sur la base sous-jacente, et l’efficacité du stockage des données. Avec Django votre application passe en premier.
Exemple :
Django : |
Salles.objects.filter(batiment__numero=107) |
SQLAlchemy : |
session.query(Salles).join(Salles, Batiment).filter(Batiment.numero==107) |
SQLA va permettre de réaliser des jointures complexes, des sous-requêtes et des agrégations complexes (Windows aggregates). Si l’on souhaite réaliser tout cela avec Django, il faudra faire appel à des requêtes en SQL pur.
Essayons donc maintenant de voir comment marier au mieux les deux : il existe des projets open source, dont le but est de marier SQLAlchemy et Django. Pour cela on peut envisager deux approches : la première consiste à essayer d’intégrer SQLA à la place de l’ORM Django, la deuxième consiste à installer SQLAlchemy à côté de l’ORM Django existant, de façon à utiliser SQLAlchemy en complément, pour par exemple réaliser certaines requêtes complexes. Concernant la première approche, les projets sont encore à l’état de démonstrateurs plus que d’outils réellement utilisables. L’ORM Django étant trop présent au cœur même de Django...