Skip to content

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.

figure63

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.

figure64

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.