1 4 8 5 mapping
Une étude de cas 5/5 : mapping objet-relationnel¶
Supports complémentaires
Pour conclure ce cours, voici une discussion sur la méthodologie d´association entre une base de données relationnelle et un langage de programmation, en supposant de plus que ce langage est orienté-objet (ce qui est très courant). Nous avons montré comment intégrer des requêtes SQL dans un langage de programmation, Python (le présent chapitre), et les mêmes principes s´appliquent à PHP, C#, C++, ou tout autre langage, objet ou non.
Cette intégration est simple à réaliser mais assez peu satisfaisante en terme d´ingénierie logicielle. Commençons par expliquer pourquoi avant de montrer des environnements de développement qui visent à éviter le problème, en associant objets et relations, en anglais object-relationnal mapping ou ORM.
Quel problème¶
Le problème est celui de la grande différence enttre deux représentations (deux modèles) des données
- dans un langage objet, les données sont sous forme d´objets, autrement dit des petits systèmes autonomes dotés de propriétés (les données) dont la structure est parfois complexe, étroitement liées à un comportement (les méthodes)
- dans une base relationnelle, les données sont des nuplets, de structure élémentaire (un dictionnaire associant des noms et des valeurs atomiques), sans aucun comportement.
- dans un langage objet, les objets sont liés les uns aux autres par un référencement physique (pointeurs ou équivalent), et une application manipule donc un graphe d´objets
- dans une base relationnelle, les nuplets sont liés par un mécanisme "logiaue" de partage de valeurs (clé primaire, clé étrangère) et on manipule des ensembles, pas des graphes.
Le problème d´une intégration entre un langage de programmation est SQL est donc celui de la conversion d´un modèle à l´autre. Ce n´était pas flagrant sur les quelques exemples simples que nous avons donnés, mais à l´échelle d´une application d´envergure, cette conversion devient pénible à coder, elle ne présente aucun intérêt applicatif, et entraine une perte de productivité peu satisfaisante.
Note
Notons au passage que pour éviter ces écarts entre modèles de données, on a beaucoup travaillé pendant une période sur les bases objets et pas relationnelles. Cette recherche n'a pas vraiment abouti à des résultats vraiment satisfaisants.
Voici un exemple un peu plus réaliste que ceux présentés jusqu´à
présent pour notre application de messagerie. Dans une approche objet,
on modéliserait nos données par des classes, soit une classe Contact
et une classe Message
. Voici pour commencer la classe Contact
, très
simple: elle ne contient que des propriétés et une méthode
d´initialisation.
class Contact:
def __init__(self,id,prenom,nom,email):
self.id=id
self.prenom=prenom
self.nom=nom
self.email=email
Et voici comment on effectue la conversion: dans une boucle sur un
curseur récupérant des contacts, on construit un objet de la classe
Contact
en lui passant comme valeurs d´initialisation celles
provenant du curseur.
curseur.execute("select * from Contact")
for cdict in curseur.fetchall():
# Conversion du dictionnaire en objet
cobj = Contact(cdict["idContact"], cdict["prénom"],
cdict["nom"], cdict["email"])
C´est la conversion la plus simple possible: elle ne prend qu´une
instruction de programmation. C´est déjà trop dans une optique de
productivité optimale: on aimerait que le curseur nous donne
directement l´objet instance de Contact
.
Les choses se gâtent avec la classe Message
dont la structure est
beaucoup plus complexe. Voici tout d´abord sa modélisation Python.
class Message:
# Emetteur: un objet 'Contact'
emetteur = Contact(0, "", "", "")
# Prédecesseur: peut ne pas exister
predecesseur = None
# Liste des destinataires = des objets 'Contacts'
destinataires = []
# La méthode d'envoi de message
def envoi(self):
for dest in self.destinaires:
L´envoi d´un message prend naturellement la forme d´une méthode de la classe. Envoyer un message devient alors aussi simple que l´instruction:
message.envoi()
Un message est un nœud dans un graphe d´objet, il est lié à un objet de
la classe Contact
(l´émetteur), et aux destinataires (objets de la
classe Contact
également). Pour instancier un objet de la classe
Message
à partir de données provenant de la base, il faut donc:
- Lire le message et le convertir en objet
- Lire l´émetteur et le convertir en objet de la classe
Contact
- Lire tous les destinataires et les convertir en objets de la classe
Contact
Cela donne beaucoup de code (je vous laisse essayer si vous les
souhaitez), d´un intérêt applicatif nul. De plus, il faudrait
idéalement qu´à chaque nuplet d´une table corresponde un seul objet
dans l´application. Avant d´instancier un objet Contact
comme
émetteur, il faudrait vérifier s´il n´a pas déjà été instancié au
préalable et le réutiliser. On aurait ainsi, pour cet objet, un lien
inverse cohérent: la liste des messages qu´il a émis. En fait, pour
l´application objet, les données ont la forme que nous avons déjà
illustrées par la Fig.63.
Fig. 63. Une instance (petite mais représentative) de notre messagerie
Bref, il faudrait que le graphe soit une image cohérente de la base, conforme à ce qui illustré par la figure, et ce n´est pas du tout facile à faire.
Quelle solution¶
Le rôle d'un système ORM est de convertir automatiquement, à la demande, la base de données sous forme d'un graphe d'objet. L'ORM s'appuie pour cela sur une configuration associant les classes du modèle fonctionnel et le schéma de la base de données. L'ORM génère des requêtes SQL qui permettent de matérialiser ce graphe ou une partie de ce graphe en fonction des besoins.
La Fig. 64 illustre l´architecture d´un système ORM. Il présente à l´application les données sous la forme d´une graphe d´objets (en haut de la figure). Ce graphe est obtenu par production automatique de requêtes SQL et conversion du résultat de ces requêtes en objets.
Fig. 64. Architecture d'un système ORM
Un système ORM s´appuie sur une configuration qui décrit la correspondance (le mapping) entre une classe et une table. Voici par exemple cette spécification pour un des systèmes ORM les plus développés, Hibernate.
@Entity(table="Message")
public class Message {
@Id
private Integer id;
@Column
private String contenu;
@ManyToOne
private Contact emetteur;
@OneToMany
private Set<Contact> destinataires ;
}
Les annotation @Entity
, @Id
, @Column
, @ManyToOne
,@OneToMany
,
indiquent au système ORM tout ce qui est nécessaire pour associer objets
et nuplets de la base. Ce même système est alors en mesure de produire
les requêtes SQL et de les soumettre au SGBD via l´interface JDBC, ou
l´API Python.
Le gain en terme de productivité est très important. Voici, toujours en Hibernate (la syntaxe est la plus claire).
List<Message> resultat =
session.execute("from Message as m "
+ " where m.emetteur.prenom = 'Serge'");
for (Message m : resultat) {
for (Contact c : m.destinataires) {
message.envoi (c.email);
}
}
Notez les deux boucles, la première sur le messages, la seconde sur
leurs destinataires. Dans le second cas, aucune requête n´a été
executée explicitement: c´est le système ORM qui s´est chargé
automatiquement de trouver les destinataires du messages et de les
présenter sous la forme d´instances de la classe Contact
.
En résumé, les systèmes ORM sont maintenant très utilisés pour les développements d´envergure. Leurs principes sont tous identiques: les accès à la base prennent la forme d´une navigation dans un graphe d´objets et le système engendre les requêtes SQL pour matérialiser le graphe. La conversion entre nuplets de la base et objets de l´application est automatique, ce qui représente un très important gain en productivité de développement. En contrepartie, tend à produire beaucoup de requêtes élémentaires là où une seule jointure serait plus efficace. Pour des bases très volumineuses, l´intervention d´un expert est souvent nécessaire afin de contrôler les requêtes engendrées.
Voici pour cette brève introduction. Pour aller plus loin, l´atelier ci-dessous propose un début de développement avec le framework Django. Vous pouvez aussi consulter le cours complet http://orm.bdpedia.fr consacré à Hibernate.