Voici une étude de cas qui va nous permettre de récapituler à peu près tout le contenu de ce cours. Nous étudions la mise en œuvre d’une base de données destinée à soutenir une application de messagerie (extrêmement simplifiée bien entendu). Même réduite aux fonctionnalités de base, cette étude mobilise une bonne partie des connaissances que vous devriez avoir assimilées. Vous pouvez vous contenter de lire le chapitre pour vérifier votre compréhension. Il est sans doute également profitable d’essayer d’appliquer les commandes et scripts présentés.

Une étude de cas

Imaginons donc que l’on nous demande de concevoir et d’implanter un système de messagerie, à intégrer par exemple dans une application web ou mobile, afin de permettre aux utilisateurs de communiquer entre eux. Nous allons suivre la démarche complète consistant à analyser le besoin, à en déduire un schéma de données adapté, à alimenter et interroger la base, et enfin à réaliser quelques programmes en nous posant, au passage, quelques questions relatives aux aspects transactionnels ou aux problèmes d’ingénierie posés par la réalisation d’applications liées à une base de données.

S1: Expression des besoins, conception

Dans un premier temps, il faut toujours essayer de clarifier les besoins. Dans la vie réelle, cela implique beaucoup de réunion, et d’allers-retours entre la rédaction de documents de spécification et la confrontation de ces spécifications aux réactions des futurs utilisateurs. La mise en place d’une base de données est une entreprise délicate car elle engage à long terme. Les tables d’une base sont comme les fondations d’une maison: il est difficile de les remettre en cause une fois que tout est en place, sans avoir à revoir du même coup tous les programmes et toutes les interfaces qui accèdent à la base.

Voici quelques exemples de besoin, exprimés de la manière la plus claire possible, et orientés vers les aspects-clé de la conception (notamment la détermination des entités, de leurs liens et des cardinalités de participation).

  • “Je veux qu’un utilisateur puisse envoyer un message à un autre”
  • “Je veux qu’il puisse envoyer à plusieurs autres”
  • “Je veux savoir qui a envoyé, qui a reçu, quel message”
  • “Je veux pouvoir répondre à un message en le citant”

Ce n’est que le début. On nous demandera sans doute de pouvoir envoyer des fichiers, de pouvoir choisir le mode d’envoi d’un message (destinataire principal, copie, copie cachée, etc.), de formatter le message ou pas, etc

On va s’en tenir là, et commencer à élaborer un schéma entité-association. En première approche, on obtient celui de la Fig. 39.

_images/ea-messagerie-1.png

Fig. 39 Le schéma de notre messagerie, première approche

Il faut nommer les entités, définir leur identifiant et les cardinalités des associations. Ici, nous avons une première ébauche qui semble raisonnable. Nous représentons des entités qui émettent des messages. On aurait pu nommer ces entités “Personne” mais cela aurait semblé exclure la possibilité de laisser une application envoyer des messages (c’est le genre de point à clarifier lors de la prochaine réunion). On a donc choisi d’utiliser le terme plus neutre de “Contact”.

Même si ces aspects terminologiques peuvent sembler mineurs, ils impactent la compréhension du schéma et peuvent donc mener à des malentendus. Il est donc important d’être le plus précis possible.

Le schéma montre qu’un contact peut envoyer plusieurs messages, mais qu’un message n’est envoyé que par un seul contact. Il manque sans doute les destinatires du message. On les ajoute donc dans le schéma de la Fig. 40.

_images/ea-messagerie-2.png

Fig. 40 Le schéma de notre messagerie, avec les destinataires

Ici, on a considéré qu’un message peut être envoyé à plusieurs contacts (cela fait effectivement partie des besoins exprimés, voir ci-dessus). Un contact peut évidemment recevoir plusieurs messages. Nous avons donc une première association plusieurs-plusieurs. On pourrait la réifier en une entité nommée, par exemple “Envoi”. On pourrait aussi qualifier l’association avec des attributs propres: le mode d’envoi par exemple serait à placer comme caractéristique de l’association, et pas du message car un même message peut être envoyé dans des modes différents en fonction du destinataire.

Voici donc le type de question à se poser, auxquelles il faut répondre en connaissance de cause: la conception, c’est un ensemble de choix qui doivent être explicites et informés.

Il nous reste à prendre en compte le fait que l’on puisse répondre à un message. On a choisi de représenter de manière générale le fait qu’un message peut être le successeur d’un autre, ce qui a l’avantage de permettre la gestion du cas des renvois et des transferts. On obtient le schéma de la Fig. 41, avec une association réflexive sur les messages.

_images/ea-messagerie.png

Fig. 41 Le schéma complet de notre messagerie

Un schéma peut donc avoir plusieurs successeurs (on peut y répondre plusieurs fois) mais un seul prédecesseur (on ne répond qu’à un seul message). On va s’en tenir là pour notre étude.

À ce stade il n’est pas inutile d’essayer de construire un exemple des données que nous allons pouvoir représenter avec cette modélisation (une “instance” du modèle). C’est ce que montre par exemple la Fig. 42.

_images/instance-messagerie.png

Fig. 42 Une instance (petite mais représentative) de notre messagerie

Sur cet exemple nous avons quatre contacts et quatre messages. Tous les cas envisagés sont représentés:

  • un contact peut émettre plusieurs messages (c’est le cas pour Serge ou Philippe)
  • un contact peut aussi recevoir plusieurs messages (cas de Sophie)
  • un message peut être envoyé à plusieurs destinataires (cas du message 4, “Serge a dit...”, transmis à Sophie et Cécile
  • un message peut être le successeur d’un (unique) autre (messages 2, 3, 4) ou non (message 1)
  • un message peut avoir plusieurs successeurs (message 1) mais toujours un seul prédecesseur.

Prenez le temps de bien comprendre comment les propriétés du modèle sont représentés sur l’instance.

Nous en restons là pour notre étude. Cela n’exclut en aucun cas d’étendre le modèle par la suite (c’est inévitable, car des besoins complémentaires arrivent toujours). Il est facile

  • d’ajouter des attributs aux entités ou aux associations existantes;
  • d’ajouter de nouvelles entités ou associations.

En revanche, il est difficile de revenir sur les choix relatifs aux entités ou aux associations déjà définies. C’est une très bonne raison pour faire appel à toutes les personnes concernées, et leur faire valider les choix effectués (qui doivent être présentés de manière franche et complète).

S2: schéma de la base

Maintenant, nous sommes prêts à implanter la base en supposant que le schéma E/A de la Fig. 42 a été validé. Avec un peu d’expérience, la production des commandes de création des tables est directe. Prenons une dernière fois le temps d’expliquer le sens des règles de passage.

Note

Pour appliquer les commandes qui suivent, vous devez disposer d’un accès à un serveur. Une base doit être créée. Par exemple:

create database Messagerie

Et vous disposez d’un utilisateur habilité à créer des tables dans cette base. Par exemple:

grant all on Messagerie.* to athénaïs identified by 'motdepasse'

On raisonne en terme de dépendance fonctionnelle. Nous avons tout d’abord celles définies par les entités.

  • \(idContact \to nom, prénom, email\)
  • \(idMessage \to contenu, dateEnvoi\)

C’est l’occasion de vérifier une dernière fois que tous les attributs mentionnés sont atomiques (email par exemple représente une seule adresse électronique, et pas une liste) et qu’il n’existe pas de dépendance fonctionnelle non explicitée. Ici, on peut trouver la DF suivante:

  • \(email \to idContact, nom, prénom\)

Elle nous dit que email est une clé candidate. Il faudra le prendre en compte au moment de la création du schéma relationnel.

Voici maintenant les dépendances données par les associations. La première lie un message au contact qui l’a émis. On a donc une dépendance entre les identifiants des entités.

  • \(idMessage \to idContact\)

Un fois acquis que la partie droite est l’identifiant du contact, le nommage est libre. Il est souvent utile d’introduire dans ce nommage la signification de l’association représentée. Comme il s’agit ici de l’émission d’un message par un contact, on peut représenter cette DF avec un nommage plus explicite.

  • \(idMessage \to idEmetteur\)

La seconde DF correspond à l’association plusieurs-à-un liant un message à celui auquel il répond. C’est une association réflexive, et pour le coup la DF \(idMessage \to idMessage\) n’aurait pas grand sens. On passe donc directement à un nommage représentatif de l’association.

  • \(idMessage \to idPrédécesseur\)

Etant entendu que idPrédécesseur est l’identifiant d’un contact. Nous avons les DF, il reste à identifier les clés. Les attributs idContact et idMessage sont les clés primaires, email est une clé secondaire, et nous ne devons pas oublier la clé définie par l’association plusieurs-plusieurs représentant l’envoi d’un message. Cette clé est la paire (idContact, idMessage), que nous nommerons plus explicitement (idDestinataire, idMessage).

Voilà, nous appliquons l’algorithme de normalisation qui nous donne les relations suivantes:

  • Contact (idContact, nom,prénon, email)
  • Message (idMessage, contenu, dateEnvoi, idEmetteur, idPrédécesseur)
  • Envoi (idDestinataire, idMessage)

Les clés primaires sont en gras, les clés étrangères (correspondant aux attributs issus des associations plusieurs-à-un) en italiques.

Nous sommes prêts à créer les tables. Voici la commande de création de la table Contact.

create table Contact (idContact integer not null,
                  nom varchar(30) not null,
                  prénom varchar(30)  not null,
                  email varchar(30) not null,
                  primary key (idContact),
                  unique (email)
               );

On note que la clé secondaire email est indiquée avec la commande unique. Rappelons pourquoi nous ne devrions pas la choisir pour clé primaire: la clé primaire d’une table est référencée par des clés étrangères dans d’autres tables. Modifier la clé primaire implique de modifier de manière synchrone les clés étrangères, ce qui peut être assez délicat.

Voici la table des messages, avec ses clés étrangères.

create table Message (
    idMessage  integer not null,
    contenu text not null,
    dateEnvoi   datetime,
    idEmetteur int not null,
    idPrédecesseur int,
    primary key (idMessage),
    foreign key (idEmetteur)
          references Contact(idContact),
    foreign key (idPrédecesseur)
       references Message(idMessage)
 )

L’attribut idEmetteur, clé étrangère, est déclaré not null, ce qui impose de toujours connaître l’émetteur d’un message. Cette contrainte, dite “de participation” semble ici raisonnable.

En revanche, un message peut ne pas avoir de prédecesseur, et idPrédécesseur peut donc être à null, auquel cas la contrainte d’intégrité référentielle ne s’applique pas.

Et pour finir, voici la table des envois.

create table Envoi (
    idDestinataire  integer not null,
    idMessage  integer not null,
    primary key (idDestinataire, idMessage),
    foreign key (idDestinataire)
           references Contact(idContact),
    foreign key (idMessage)
           references Message(idMessage)
 )

C’est la structure typique d’une table issue d’une association plusieurs-plusieurs. La clé est composite, et chacun de ses composants est une clé étrangère.

S3: requêtes

Pour commencer, nous devons peupler la base. À titre d’exercice, essayons de créer l’instance illustrée par la Fig. 42. Les commandes qui suivent correspondent aux deux premiers messages, les autres sont laissés à titre d’exercice.

Il nous faut d’abord au moins deux contacts.

insert into Contact (idContact, prénom, nom,  email)
  values (1, 'Serge', 'A.', 'serge.a@inria.fr');
insert into Contact (idContact, prénom, nom,  email)
  values (4, 'Philippe', 'R.', 'philippe.r@cnam.fr');

L’insertion du premier message suppose connue l’identifiant de l’emetteur. Ici, c’est Philippe R., dont l’identifiant est 4. Les messages eux-mêmes sont (comme les contacts) identifiés par un numéro séquentiel.

insert into Message (idMessage, contenu, idEmetteur)
values (1, 'Hello Serge', 4);

Note

Laisser l’utilisateur fournir lui-même l’identifiant n’est pas du tout pratique. Il faudrait mettre en place un mécanisme de séquence, dont le détail dépend (malheureusement) du SGBD.

Et la définition du destinataire.

insert into Envoi (idMessage, idDestinataire) values (1, 1);

La date d’envoi n’est pas encore spécifiée (et donc laissée à null) puisque la création du message dans la base ne signifie pas qu’il a été envoyé. Ce sera l’objet des prochaines sessions.

Nous pouvons maintenant insérer le second message, qui est une réponse au premier et doit donc référencer ce dernier comme prédécesseur. Cela suppose, encore une fois, de connaître son identifiant.

insert into Message (idMessage, contenu, idEmetteur, idPrédecesseur)
values (2, 'Coucou Philippe', 1, 1);

On voit que la plupart des données fournies sont des identifiants divers, ce qui rend les insertions par expression directe de requêtes SQL assez pénibles et surtout sujettes à erreur. Dans le cadre d’une véritable application; ces insertions se font après saisie via une interface graphique qui réduit considérablement ces difficultés.

Nous n’avons plus qu’à désigner le destinataire de ce deuxième message.

insert into Envoi (idMessage, idDestinataire)
values (2, 4);

Bien malin qui, en regardant ce nuplet, pourrait deviner de quoi et de qui on parle. Il s’agit purement de la définition d’un lien entre un message et un contact.

Voici maintenant quelques exemples de requêtes sur notre base. Commençons par chercher les messages et leur émetteur.

select idMessage, contenu, prénom, nom
from Message as m,  Contact as c
where m.idEmetteur = c.idContact

Comme souvent, la jointure associe la clé primaire (de Contact) et la clé étrangère (dans le message). La jointure est l’opérqtion inverse de la normalisation: elle regroupe, là ou la normalisation décompose.

On obtient le résultat suivant (en supposant que la base correspond à l’instance de la Fig. 42).

idMessage contenu prénom nom
1 Hello Serge Philippe R
2 Coucou Philippe Serge A
3 Philippe a dit ... Serge A
4 Serge a dit ... Philippe R

Cherchons maintenant les messages et leur prédecesseur.

select m1.contenu as 'Contenu', m2.contenu as 'Prédecesseur'
from Message as m1,  Message as m2
where m1.idPrédecesseur = m2.idMessage

Ce qui donne:

Contenu Prédecesseur
Coucou Philippe Hello Serge
Philippe a dit ... Hello Serge
Serge a dit ... Coucou Philippe

Quelle est la requête (si elle existe...) qui donnerait la liste complète des prédecesseurs d’un message? Réflechissez-y, la question est épineuse et fera l’objet d’un travail complémentaire.

Et voici une requête d’agrégation: on veut tous les messages envoyés à plus d’un contact.

select m.idMessage, contenu, count(*) as 'nbEnvois'
from Message as m, Envoi as e
where m.idMessage = e.idMessage
group by idMessage, contenu
having nbEnvois > 1

Si une requête est un tant soit peu compliquée et est amenée à être excutée souvent, ou encore si le résultat de cette requête est amené à servir de base à des requêtes complémentaires, on peut envisager de créer une vue.

create view EnvoisMultiples as
select m.idMessage, contenu, count(*) as 'nbEnvois'
from Message as m, Envoi as e
where m.idMessage = e.idMessage
group by idMessage, contenu
having nbEnvois > 1

Pour finir, un exemple de mise à jour: on veut supprimer les messages anciens, disons ceux antérieurs à 2015.

delete from Message; where year(dateEnvoi) < 2015

Malheureusement, le système nous informe qu’il a supprimé tous les messages:

All messages deleted. Table message is now empty..

Que s’est-il passé? Un point virgule mal placé (vérifiez). Est-ce que tout est perdu? Non, réfléchissez et trouvez le bon réflexe. Cela dit, les mises à jour et destructions devraient être toujours effectuées dans un cadre très contrôlé, et donc par l’intermédiaire d’une application.

S4: Programmation (Python)

Voici maintenant quelques exemples de programmes accédant à notre base de données. Nous reprenons notre hypothèse d’une base nommée ‘’Messagerie”, gérée par un SGBD relationnel (disons, ici, MySQL). Notre utilisatrice est Athénaïs: elle va écrire quelques scripts Python pour exécuter ses requêtes (Fig. 43).

Note

Le choix de python est principalement motivé par la concision et la simplicité. On trouverait à peu près l’équivalent des programmes ci-dessous dans n’importe quel langage. L’interface Python/MySQL illustrée ici est à peu près standard pour tous les SGBD et nos scripts fonctionneraient sans doute à peu de chose près avec Postgres ou un autre.

Les scripts que nous allons exécuter sont des programmes clients, qui peuvent s’exécuter sur une machine, se connecter par le réseau au serveur de données, auquel ils transmettent des commandes (principalement des requêtes SQL). Nous sommes dans l’architecture très classique de la Fig. 43.

_images/prog-python.png

Fig. 43 Architecture d’un programme dialoguant avec un serveur

Un programme de lecture

Pour établir une connexion, tout programme client doit fournir au moins 4 paramètres: l’adresse de la machine serveur (une adresse IP, ou le nom de la machine), le nom et le mot de passe de l’utilisateur qui se connecte, et le nom de la base. On fournit souvent également des options qui règlent certains détails de communication entre le client et le serveur. Voici donc la connexion à MySQL avec notre programme Python.

connexion = pymysql.connect
        ('localhost',
         'athénaïs',
         'motdepasse',
         'Messagerie',
          cursorclass=pymysql.cursors.DictCursor)

Ici, on se connecte à la machine locale sous le compte d’Athénaïs, et on accède à la base Messagerie. Le dernier paramètre est une option cursorClass qui indiquent que les données (nuplets) retournés par le serveur seront représentés par des dictionnaires Python.

Note

Un dictionnaire est une structure qui associe des clés (les noms des attributs) et des valeurs. Cette structure est bien adaptée à la représentation des nuplets.

Un curseur est créé simplement de la manière suivante:

curseur = connexion.cursor()

Une fois que l’on a créé un curseur, on s’en sert pour exécuter une requête.

curseur.execute("select * from Contact")

À ce stade, rien n’est récupéré côté client. Le serveur a reçu la requête, a créé le plan d’exécution et se tient prêt à fournir des données au client dès que ce dernier les demandera. Comme nous l’avons vu dans le chapitre sur la programme, un curseur permet de parcourir le résultat d’une requête, qui est obtenu avec la commande fetchAll(). Le code Python pour parcourir tout le résultat est donc:

for contact in curseur.fetchall():
   print(contact['prénom'], contact['nom'])

La boucle affecte, à chaque itération, le nuplet courant à la variable contact. Cette dernière est donc un dictionnaire dont chaque entrée associe le nom de l’attribut et sa valeur.

Et voilà. Pour résumer, voici le programme complet, qui est donc remarquablement concis.

import pymysql
import pymysql.cursors

connexion = pymysql.connect('localhost', 'athénaïs',
                     'motdepasse', 'Messagerie',
                     cursorclass=pymysql.cursors.DictCursor)

curseur = connexion.cursor()
curseur.execute("select * from Contact")

for contact in curseur.fetchall():
    print(contact['prénom'], contact['nom'])

Bienb entendu, il faudrait ajouter un petit travail d’ingénierie pour ne pas donner les paramètres de connexion sous forme de constante mais les récupérer dans un fichier de configuration, et ajouter le traitement des erreurs (par exemple un refus de connexion).

Une transaction

Notre second exemple montre une transaction qui sélectionne tous les messages non encore envoyés, les envoie, et marque ces messages en leur affectant la date d’envoi. Voici le programme complet (à l’exception de la connexion, , suivi de quelques commentaires.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
     import pymysql
     import pymysql.cursors
     from datetime import datetime

     connexion = pymysql.connect('localhost', 'athénaïs',
                      'motdepasse', 'Messagerie',
                      cursorclass=pymysql.cursors.DictCursor)

     # Tous les messages non envoyés
     messages = connexion.cursor()
     messages.execute("select * from Message where dateEnvoi is null")
     for message in messages.fetchall():
         # Marquage du message
         connexion.begin()
         maj = connexion.cursor()
         maj.execute ("Update Message set dateEnvoi='2018-12-31' "
             + "where idMessage=%s", message['idMessage'])

         # Ici on envoie les messages à tous les destinataires
         envois = connexion.cursor()
         envois.execute("select * from Envoi as e, Contact as c "
                +" where e.idDestinataire=c.idContact "
                + "and  e.idMessage = %s", message['idMessage'])
         for envoi in envois.fetchall():
             mail (envoi['email'], message['contenu')

         connexion.commit()

Donc, ce programme effectue une boucle sur tous les messages qui n’ont pas de date d’envoi (lignes 10-12). À chaque itération, le cursor affecte une variable message.

Chaque passage de la boucle donne lieu à une transaction, initiée avec connexion.begin() et conclue avec connexion.commit(). Cette transaction effectue en tout et pour tout une seule mise à jour, celle affectant la date d’envoi au message (il faudrait bien entendu trouver la date du jour, et ne pas la mettre “en dur”).

Dans la requête update (lignes 16-17), notez qu’on a séparé la requête SQL et ses paramètres (ici, l’identifiant du message). Cela évite de construire la requête comme une chaîne de caractères.

On ouvre ensuite un second curseur (lignes 20-24), sur les destinataires du message, et on envoie ce dernier. Une remarque importante: les données traitées (message et destinataires) pourraient être récupérées en une seule requête SQL par une jointure. Mais le format du résultat (une table dans laquelle le message est répété avec chaque destinataire) ne convient pas du tout à la structure du programme dont la logique consiste à récupérer d’abord le message, puis à parcourir les envois, en deux requêtes. En d’autres termes, dans ce type de programme (très courant), SQL est sous-utilisé. Nous revenons sur ce point dans la dernière session.

S5: aspects transactionnels

Reprenons le programme transactionnel d’envoi de message. Même sur un exemple aussi simple, il est utile de se poser quelques questions sur ses propriétés dans un environnement sujet aux pannes et à la concurrence.

Une exécution de ce programme crée une transaction par message. Chaque transaction: lit un message sans date d’envoi dans le curseur, envoie le message, puis modifie le message dans la base en affectant la date d’envoi. La transaction se termine par un commit. Que peut-on en déduire, en supposant un environnement idéal sans panne, où chaque transaction est seule à s’exécuter quand elle s’exécute? Dans un tel cas, il est facile de voir que chaque message serait envoyé exactement une fois. Les choses sont moins plaisantes en pratique, regardons-y de plus près.

Cas d’une panne

Imaginons (pire scénario) une panne juste avant le commit, comme illustré sur la Fig. 44. Cette figure montre la phase d’exécution, suivie de la séquence des transactions au sein desquelles on a mis en valeur celle affectant le message \(M_1\).

_images/transaction-messages-1.png

Fig. 44 Cas d’une panne en cours de transaction

Au moment de la panne, le SGBD va effectuer un rollback qui affecte la transaction en cours. Le message reprendra donc son statut initial, sans date d’envoi. Il a pourtant été envoyé: l’envoi n’étant pas une opération de base de données, le SGBD n’a aucun moyen de l’annuler (ni même d’ailleurs de savoir quelle action le programme client a effectuée). C’est donc un premier cas qui viole le comportement attendu (chaque message envoyé exactement une fois).

Il faudra relancer le programme en espérant qu’il se déroule sans panne. Cette seconde exécution ne sélectionnera pas les messages traités par la première exécution avant \(M_1\) puisque ceux-là ont fait l’objet d’une transaction réussie. Selon le principe de durabilité, le commit de ces transactions réussies n’est pas affecté par la panne.

Le curseur est-il impacté par une mise à jour?

Passons maintenant aux problèmes potentiels liés à la concurrence. Supposons, dans un premier scénario, qu’une mise à jour du message \(M_1\) soit effectuée par une autre transaction entre l’exécution de la requête et le traitement de \(M_1\). La Fig. 45 montre l’exécution concurrente de deux exécutions du programme d’envoi: la première transaction (en vert) modifie le message et effectue un commit avant la lecture de ce message par la seconde (en orange).

_images/transaction-messages-2.png

Fig. 45 Cas d’une mise à jour après exécution de la requête mais avant traitement du message

Question: cette mise à jour sera-t-elle constatée par la lecture de \(M_1\)? Autrement dit, est-il possible que l’on constate, au moment de lire ce message dans la transaction orange, qu’il a déjà une date d’envoi parce qu’il a été modifié par la transaction verte?

On pourrait être tenté de dire “Oui” puisqu’au moment où la transaction orange débute, le message a été modifié et validé. Mais cela voudrait dire qu’un curseur permet d’accéder à des données qui ne correspondent pas au critère de sélection ! (En l’occurrence, on s’attend à ne recevoir que des messages sans date d’envoi). Ce serait très incohérent.

En fait, tout se passe comme si le résultat du curseur était un “cliché” pris au moment de l’exécution, et immuable durant tout la durée de vie du curseur. En d’autres termes, même si le parcours du résultat prend 1 heure, et qu’entretemps tous les messages ont été modifiés ou détruits, le système continuera à fournir via le curseur l’image de la base telle qu’elle était au moment de l’exécution.

En revanche, si on exécutait à nouveau une requête pour lire le message juste avant la modification de ce dernier, on verrait bien la mise à jour effectuée par la transaction verte. En résumé: une requête fournit la version des nuplets effective, soit au moment où la requête est exécutée (niveau d’isolation read committed), soit au moment où la transaction début (niveau d’isolation repeatable read).

Conséquence: sur le scénario illustré par la Fig. 45, on enverra le message deux fois. La seule manière d’éviter ce scénario serait de verrouiller tous les nuplets sélectionnés au moment de l’exécution, et d’effectuer l’ensemble des mises à jour en une seule transaction.

Transactions simultanées

Voici un dernier scénario, montrant un exécution simultanée ou quasi-simultanée de deux transactions concurrentes affectant le même message (Fig. 46).

_images/transaction-messages-3.png

Fig. 46 Exécution concurrente, avec risque de deadlock

Cette situation est très peu probable, mais pas impossible. Il correspond au cas-type dit “des mises à jour perdues” étudié” dans le chapitre sur les transactions. Dans tous les niveaux d’isolation sauf serializable, le déroulé sera le suivant:

  • chaque transaction lit séparément le message
  • une des transactions, disons la verte effectue une mise à jour
  • la seconde transaction (orange) tente d’effectuer la mise à jour et est mise en attente;
  • la transaction verte finit par effectuer un commit, ce qui libère la transaction orange: le message est envoyé deux fois.

En revanche, en mode serializable, chaque transaction va bloquer l’autre sur le scénario de la Fig. 46. Le système va détecter cet interblocage et rejeter une des transactions.

La bonne méthode

Ce qui précède mène à proposer une version plus sûre dun programme d’envoi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 # Tous les messages non envoyés
 messages = connexion.cursor()
 messages.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE")

 # Début de la transaction
 connexion.begin()
 messages.execute("select * from Message where dateEnvoi is null")

 for message in messages.fetchall():
     # Marquage du message
     maj = connexion.cursor()
     maj.execute ("Update Message set dateEnvoi='2018-12-31' "
             + "where idMessage=%s", message['idMessage'])

     print ("Envoi du message ...", message['contenu'])

 connexion.commit()

Tout d’abord (ligne 3) on se place en niveau d’isolation sérialisable.

Puis (ligne 5), on débute la transaction à l’extérieur de la boucle du curseur, et on la termine après la boucle (ligne 17). Cela permet de traiter la requête du curseur comme partie intégrante de la transaction.

Au moment de l’exécution du curseur, les nuplets sont réservés, et une exécution simultanée sera mise en attente si elle essaie de traiter les mêmes messages.

Avec cette nouvelle version, la seule cause d’envoi multiple d’un message et l’occurence d’un panne. Et le problème dans ce cas vient du fait que l’envoi n’est pas une opération contrôlée par le serveur de données.

S6: mapping objet-relationnel

Quel problème, quelle solution?

Démonstration d’un framework complet: Django

pip3 install django
python3

  >>> import django
  >>> print(django.__path__)
  >>> print(django.get_version())

Django est installé avec un utilitaire texttt{django-admin}.

django-admin startproject monappli

On peut lancer un serveur

cd monappli
python3 manage.py runserver

Le serveur est en écoute sur le port 8000 de votre machine. Vous pouvez accéder à l’URL http://localhost:8000 avec votre navigateur.

À partir de là, on peut travailler sur le projet avec un éditeur de texte ou (mieux) un IDE comme Eclipse.

Un projet Django est un ensemble d’applications

_images/inidjango.png

Fig. 47 La première page de l’application

Au départ, une application nommée comme le projet: elle contient la configuration. Créons notre première application} avec la commande suivante:

python3 manage.py startapp messagerie
_images/django-newapp.png

Fig. 48 Nouvelle application

Ensuite, il faut ajouter l’application messagerie dans monappli/monappli/settings.py.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "messagerie"
]

Nos modèles

create database Monappli;
grant all on Monappli.* to philippe identified by 'motdepasse'

On définit des classes représentant les objets de la base.

Django se charge de créer et de maintenir la base de données. Génération des requêtes SQL:

python3 manage.py makemigrations messagerie

Contrôle des requêtes SQL

python3  manage.py sqlmigrate messagerie 0001

Exécution des requêtes SQL

python3 manage.py migrate

Django mémorise toutes les évolutions de la base et produit les commandes pour les exécuter.

Django a créé une interface Web d’administration des données!

Commençons par créer un super-utilisateur.

python3 manage.py createsuperuser

Indiquer qu’on veut engendrer une interface sur les questions: on ajoute les lignes suivantes dans messagerie/admin.py.

from django.contrib import admin

from .models import Message, Contact, Envoi

admin.site.register(Contact)
admin.site.register(Message)
admin.site.register(Envoi)

L’interface d’administration est automatiquement engendrée et gérée par le framework.

admin-django

Passons aux vues

La notion de alert{vue} dans Django correspond plutôt à la notion de alert{contrôleur} dans d’autres frameworks.

vfill

Une vue est constituée alert{d’actions}. Dans texttt{polls/views.py}

from django.http import HttpResponse

def index(request):
    return HttpResponse("Je suis l'application 'polls', action 'index'.")

Une action reçoit une alert{requête} (HTTP) et retourne une alert{réponse} (HTTP).

Les actions et leurs URLs. Chaque action est associée à une URL

Dans texttt{monappli/urls.py}.

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^messagerie/', include('messagerie.urls')),
]

Et dans texttt{messagerie/urls.py}.

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

Dernière étape: les templates}

Template = un fragment HTML, avec des commandes pour afficher un alert{contexte.}

vfill

Créer le fichier texttt{polls/templates/polls/index.html}.

{% if messages %}
  <ul>
     {% for message in messages %}
        <li>
         {{ message.contenu }}
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>Aucun message.</p>
{% endif %}

“Contexte” = des données passées au template par l’action.

L’action crée le contexte et appelle le template

Redéfinir l’action texttt{index} dans texttt{messagerie/views.py} de la manière suivante:

def index(request):
    messages = Message.objects.all()
    context = {'messages': messages}

    return render(request, 'messagerie/index.html', context)