IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction à Python 3 et au framework web Flask par l'exemple


précédentsommairesuivant

19. Utilisation de l’ORM SQLALCHEMY

Le chapitre précédent a montré que dans certains cas on pouvait écrire du code indépendant du SGBD utilisé avec l’architecture suivante :

Image non disponible

Dans ce chapitre nous allons utiliser l’ORM (Object Relational Mapper) [sqlalchemy] pour accéder aux SGBD de façon uniforme quelque soit le SGBD utilisé. Un ORM permet deux choses :

  • il permet à un script de dialoguer avec le SGBD sans émettre d’ordres SQL ;

  • il masque au script les particularités de chaque SGBD ;

L’architecture devient la suivante :

Image non disponible

Le script est désormais séparé des connecteurs par l’ORM. Il dialogue avec l’ORM avec des classes et des méthodes. Il n’exécute pas de code SQL. C’est l’ORM qui le fait avec les connecteurs auxquels il est relié. Il cache au script les particularités de ces connecteurs. Aussi le code du script est-il insensible à un changement de connecteur (donc du SGBD) ;

L’arborescence des scripts étudiés sera la suivante :

Image non disponible

19-1. Installation de l’ORM [sqlalchemy]

L’ORM [sqlalchemy] vient sous la forme d’un package python qu’il faut installer dans un terminal Python :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\sqlalchemy>pip install sqlalchemy
Collecting sqlalchemy
  Downloading SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl (1.2 MB)
     || 1.2 MB 3.3 MB/s
Installing collected packages: sqlalchemy
Successfully installed sqlalchemy-1.3.18

19-2. Scripts 01 : les bases

Image non disponible
  • en [1], les scripts qui vont être étudiés. Ces scripts vont utiliser les classes de [2] : BaseEntity, MyException, Personne, Utils ;

19-2-1. Configuration

Le fichier [config] configure l’application de la façon suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
def configure():
    # root_dir
    # chemin absolu référence des chemins relatifs de la configuration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # chemins absolus des dépendances
    absolute_dependencies = [
        # BaseEntity, MyException, Personne, Utils
        f"{root_dir}/classes/02/entities",
    ]

    # on fixe le syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # configuration des classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # on rend la config
    return {}

Commentaires

  • ligne 8 : on met dans le Python Path le dossier contenant les classes [BaseEntity, MyException, Personne, Utils] ;

  • lignes 12-13 : on fixe le Python Path de l’application ;

  • lignes 16-17 : on se rappelle peut-être que la classe BaseEntity a un attribut de classe nommé [excluded_keys]. Cet attribut est une liste dans laquelle on met les propriétés de la classe qu’on ne veut pas voir apparaître dans le dictionnaire de celle-ci (fonction asdict). Ici on exclut la propriété [_sa_instance_state] de l’état de la classe [Personne]. On verra bientôt pourquoi ;

19-2-2. Script [démo]

Le script [démo] montre une première utilisation de l’ORM [sqlalchemy] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
# on récupère la configuration de l'application
import config

config = config.configure()

# imports
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper

from Personne import Personne

# metadata
metadata = MetaData()

# la table
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )
# la classe Personne avant le mapping
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")

# le mapping
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'prénom': personnes_table.c.prenom,
    'nom': personnes_table.c.nom,
    'âge': personnes_table.c.age
})

# personne1 n'a pas été modifiée
print(f"personne1={personne1.__dict__}")
# la classe Personne a elle été modifiée - elle a été "enrichie"
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

Commentaires

  • lignes 1-4 : on configure l’application ;

  • lignes 6-10 : on importe les modules nécessaires au script ;

  • ligne 13 : [MetaData] est une classe de [sqlalchemy] ;

  • lignes 15-22 : [Table] est une classe de [sqlalchemy]. Elle permet de décrire une table d’une base de données. Ici, nous allons décrire la table [personnes] de la base MySQL [dbpersonnes] étudiée au chapitre MySQL ;

    • ligne 16 : le 1er paramètre [personnes] est le nom de la table décrite ;

    • ligne 16 : le second paramètre [metadata] est l’instance [MetaData] créée ligne 13 ;

    • lignes 17-22 : chacun des paramètres suivants décrit une colonne de la table avec une syntaxe propre à [sqlalchemy] mais proche de la syntaxe SQL ;

    • chaque colonne se décrit avec une instance de la classe [Column] de [sqlalchemy] ;

      • le 1er paramètre est le nom de la colonne ;

      • le second paramètre est son type ;

      • les paramètres suivants sont des paramètres nommés :

        • ligne 17 : [primary_key=True] pour indiquer que la colonne [id] est clé primaire de la table [personnes] ;

        • ligne 18 : [nullable=False] pour indiquer qu’uen colonne doit forcément avoir une valeur lorsqu’une ligne est insérée dans la table ;

    • ligne 21 : enfin la classe [UniqueConstraint] permet de décrire une contrainte d’unicité. Ici on indique que les colonnes (nom, prenom) doivent être uniques dans la table. La propriété nommée [name] permet de donner un nom à cette constrainte. Ici, il faut distinguer deux cas :

      • on décrit une table existante. Il faut alors chercher le nom de la contrainte dans les propriétés de la table (phpMyAdmin ou pgAdmin) ;

      • on décrit une table que l’on va créer. Alors on met le nom que l’on veut ;

  • lignes 23-25 : on crée une personne [personne1] et on affiche son dictionnaire [__dict__]. On va avoir ici :

     
    Sélectionnez
    1.
    personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
    
  • lignes 27-33 : on fait un mapping, ç-à-d qu’on crée une correspondance entre la classe [Personne] et la table [personnes]. C’est essentiellement une correspondance [propriétés de la classe  colonnes de la table]. La fonction [mapper] accepte ici trois paramètres :

    • ligne 28 : le 1er paramètre est le nom de la classe pour laquelle on fait le mapping ;

    • ligne 28 : le second paramètre est la table à laquelle elle va être associée. Celle-ci est l’objet [Table] créé ligne 16 ;

    • ligne 28 : le 3ième paramètre est ici un paramètre nommé [properties]. C’est un dictionnaire dans lequel les clés sont les propriétés de la classe mappée et les valeurs les colonnes de la table mappée. Pour désigner la colonne X de la table [personnes_table], on écrit [personnes_table.c.X] ;

  • lignes 35-36 : on réaffiche la personne [personne1] une fois le mapping fait. On constate qu’elle n’a pas changé :

     
    Sélectionnez
    1.
    personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
    
  • lignes 37-39 : on crée une nouvelle personne [personne2] et on l’affiche. On a alors l’affichage suivant :

     
    Sélectionnez
    1.
    personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
    

    On constate que le dictionnaire [__dict__] a été profondément modifié :

    • une nouvelle propriété [_sa_instance_state] apparaît. On voit que c’est un objet de l’ORM [sqlalchemy] ;

    • les autres propriétés ont été débarrassées de leur préfixe qui indiquait à quelle classe elles appartenaient ;

    On peut donc conclure que l’opération de mapping des lignes 27-33 a modifié la classe [Personne].

Lorsqu’on voudra afficher l’état d’un objet [Personne], on ne voudra pas en général de la propriété [_sa_instance_state]. Elle n’est là en effet que pour la cuisine interne de [sqlalchemy] et en général elle ne nous intéresse pas. C’est pourquoi on a écrit dans le script [config] :

 
Sélectionnez
1.
2.
3.
    # configuration des classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

19-2-3. Le script [main]

Le script [main] va manipuler la table [personnes] de la base MySQL [dbpersonnes] en s’interfaçant avec [sqlalchemy]. Pour comprendre la suite, il faut se rappeler l’architecture utilisée ici :

Image non disponible

Si [Database1] est la base [dbpersonnes], on voit que la liaison entre le script et cette base passe par deux entités :

  • le connecteur Python au SGBD MySQL ;

  • le SGBD MySQL ;

Le script [main] va dialoguer avec l’ORM qui va ensuite dialoguer avec le connecteur Python. L’ORM dialogue avec ce connecteur avec les outils décrits dans les paragraphes MySQL et PostgreSQL notamment en émettant des ordres SQL. Le script [main] ne va pas utiliser d’ordres SQL. Il va s’appuyer sur l’API (Application Programming Interface) de l’ORM, faite de classes et d’interfaces.

Le script [main] est le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
# on configure l'application
import config

config = config.configure()

# imports
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.exc import IntegrityError, InterfaceError
from sqlalchemy.orm import mapper, sessionmaker

from Personne import Personne

# chaîne de connexion à une base de données MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

# metadata
metadata = MetaData()

# la table
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )

# le mapping
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'prénom': personnes_table.c.prenom,
    'nom': personnes_table.c.nom,
    'âge': personnes_table.c.age
})

# la session factory
Session = sessionmaker()
Session.configure(bind=engine)

session = None
try:
    # une session
    session = Session()

    # suppression de la table [personnes]
    session.execute("drop table if exists personnes")

    # recréation de la table à partir du mapping
    metadata.create_all(engine)

    # une insertion
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

    # une requête
    personnes = session.query(Personne).all()

    # affichage
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # deux autres insertions dont la seconde échoue à cause de l'unicité (prénom,nom)
    session.add(Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 10}))
    session.add(Personne().fromdict({"id": 69, "prénom": "x1", "nom": "y1", "âge": 10}))

    # une requête
    personnes = session.query(Personne).all()

    # affichage
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # validation de la session
    session.commit()

except (InterfaceError, IntegrityError) as erreur:
    # affichage
    print(f"L'erreur suivante s'est produite : {erreur}")
    # annulation de la dernière session
    if session:
        print("rollback...")
        session.rollback()
finally:
    # on libère les ressources de la session
    if session:
        session.close()

Commentaires

  • lignes 1-4 : l’application est configurée ;

  • lignes 7-9 : on importe toute une série de classes et d’interfaces de la bibliothèque [sqlalchemy] ;

  • ligne 11 : la classe [Personne] est importée ;

  • ligne 14 : la chaîne de connexion à la base de données. Elle précise :

    • le SGBD utilisé (mysql) ;

    • le connecteur Python utilisé (mysql.connector sans le .) ;

    • l’utilisateur qui se connecte (admpersonnes) ;

    • son mot de passe (nobody) ;

    • la machine sur laquelle se trouve le SGBD (localhost=machine sur laquelle se trouve le script qui s’exécute) ;

    • le nom de la base de données (dbpersonnes) ;

    Avec ces informations, [sqlalchemy] peut se connecter à la base de données. A noter que le connecteur Python utilisé doit être déjà installé. [sqlalchemy] ne le fait pas.

  • lignes 19-26 : description de la table [personnes] ;

  • lignes 28-34 : mapping entre la classe [Personne] et la table [personnes] ;

  • lignes 36-38 : la plupart des opérations [sqlalchemy] se font dans une session. La notion de session [sqlalchemy] est proche de celle de transaction SQL. Les sessions sont crées à partir de la classe [Session] rendue par la fonction [sessionmaker] de la ligne 37 ;

  • ligne 38 : la classe [Session] est associée à la base [dbpersonnes] via la chaîne de connexion de la ligne 14 ;

  • ligne 43 : on crée une session. Comme il a été dit, on peut rapprocher une session d’une transaction ;

  • lignes 45-46 : la méthode [Session.execute] permet d’exécuter un ordre SQL. Ce n’est pas quelque chose de courant puisqu’on a dit que l’ORM permettait d’éviter le langage SQL ;

  • lignes 48-49 : la méthode [metadata.create_all] permet de créer toutes les tables utilisant l’instance [MetaData] de la ligne 17. Nous n’en avons qu’une : la table [personnes] définie lignes 20-26. [sqlalchemy] va utiliser l’information de ces lignes pour créer la table. On a là un premier intérêt de l’ORM : il cache les spécificités des SGBD. En effet, l’ordre SQL [create] peut être très différent d’un SGBD à l’autre à cause des types donnés aux colonnes. Il n’y a pas eu d’uniformisation SQL des types de données. Ainsi l’ordre [create] varie d’un SGBD à l’autre. Ici, grâce à [sqlalchemy] :

    • nous décrivons de façon unique la table que nous désirons ;

    • [sqlalchemy] se débrouille pour générer le [create] qui va bien pour le SGBD qu’il a en face de lui ;

  • ligne 52 : on ajoute un objet [Personne] à la session. Cela ne l’ajoute pas automatiquement en base de données. En effet, un ORM suit ses propres règles pour se synchroniser avec la base de données. Il va toujours chercher à optimiser le nombre de requêtes qu’il fait. Prenons un exemple. Le script ajoute (add) deux personnes (personne1, personne2) dans la session puis fait ensuite une requête : il veut voir toutes les personnes présentes dans la table. [sqlalchemy] peut procéder ainsi :

    • l’ajout de [personne1] peut se faire en mémoire. Il n’y a pas besoin pour l’instant de le mettre en base de données ;

    • idem pour [personne2] ;

    • vient ensuite la requête de type [select]. Il faut alors récupérer toutes les lignes de la table [personnes]. [sqlalchemy] va alors mettre [personne1, personne2] en base puis faire la requête ;

    [sqlalchemy] va ainsi faire des optimisations transparentes pour le développeur.

  • ligne 56 : pour faire une requête de type [select] (je veux voir …), on utilise la méthode [Session.query]. Le paramètre de la méthode [query] est la classe mappée avec la table interrogée. Cette méthode rend un type [Query]. La méthode [Query.all] demande tous les objets [Personne] de la session. On lui ramène toutes les lignes de la table [personnes], chacune sous la forme d’un objet [Personne]. Pour faire cela, [sqlalchemy] utilise le mapping qui a été fait entre la classe [Personne] et la table [personnes]. Le résultat de la ligne 56 est une liste d’objets [Personne] ;

  • lignes 58-61 : on affiche les éléments de la liste [personnes]. Parce que la classe [Personne] dérive de la classe [BaseEntity], la méthode [Personne.__str__] utilisée ici implicitement dans la ligne 61, est en fait la méthode [BaseEntity.__str__] qui rend la chaîne jSON de l’objet appelant. Cette chaîne est la chaîne jSON du dictionnaire [Personne.asdict] (cf. BaseEntity). Nous avons dit qu’après le mapping, on allait trouver la propriété [_sa_instance_state] dans chaque objet [Personne]. Or la valeur de cette propriété n’est pas un type [BaseEntity]. Il faut donc l’exclure du dictionnaire de la classe [Personne] sinon l’affichage ‘plante’. C’est ce qui a été fait dans le script [config] ;

  • lignes 63-65 : on ajoute deux autres personnes qui ont les mêmes nom et prénom. Or on a une contrainte d’unicité sur l’union de ces deux colonnes. Une erreur devrait donc se produire. C’est ce qu’on cherche à voir ;

  • lignes 67-68 : on demande de nouveau la liste de toutes les personnes de la base ;

  • lignes 70-73 : et on les affiche ;

  • lignes 75-76 : la session est validée ‘commitée’. Comme son nom l’indique, la transaction sous-jacente va être validée ;

  • on va voir à l’exécution que les lignes 67-76 ne vont pas être exécutées à cause de l’exception produite par la ligne 65. On va alors aller aux lignes 78-84 pour gérer l’exception ;

  • ligne 78 : l’exception [InterfaceError] se produit si [sqlalchemy] n’arrive pas à se connecter à la base de données [dbpersonnes]. L’exception [IntegrityError] se produit ligne 65 ;

  • ligne 80 : on affiche l’erreur ;

  • lignes 82-84 : si la session existe, on l’annule. Cela revient à annuler la transaction sous-jacente ;

  • lignes 85-88 : dans tous les cas, erreur ou pas, la session est fermée pour libérer des ressources ;

Les résultats de l’exécution sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/01/main.py
Liste des personnes ---------
{"nom": "y", "prénom": "x", "id": 67, "âge": 10}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y1-x1' for key 'uix_1'
[SQL: INSERT INTO personnes (id, prenom, nom, age) VALUES (%(id)s, %(prenom)s, %(nom)s, %(age)s)]
[parameters: ({'id': 68, 'prenom': 'x1', 'nom': 'y1', 'age': 10}, {'id': 69, 'prenom': 'x1', 'nom': 'y1', 'age': 10})]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...

Process finished with exit code 0
  • lignes 2-3 : la liste des personnes après la 1ère insertion ;

  • ligne 5 : l’exception [IntegrityError] qui s’est produite lorsqu’on a ajouté deux personnes ayant les mêmes nom et prénom ;

  • lignes 6-7 : on notera l’ordre SQL qui a échoué. C’est un ordre INSERT paramétré : [sqlalchemy] a inséré les deux personnes avec un unique INSERT. On voit là qu’il a essayé d’optimiser les ordres SQL émis ;

Maintenant allons-voir, avec phpMyAdmin, le contenu de la table [personnes] :

Image non disponible

On voit en [6] que la table est vide. Il n’y a même pas la 1ère personne que le script avait mis dans la session. Ceci parce que celle-ci se déroulait dans une transaction et que celle-ci a été défaite dans la clause [except] du script [main].

Procédons maintenant à la modification suivante dans [main] :

 
Sélectionnez
1.
2.
3.
    # une insertion
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

Après avoir ajouté une personne ligne 2, nous décommentons la ligne 3. L’opération [session.commit] va valider la transaction sous-jacente et une nouvelle transaction va démarrer. Après exécution, le contenu de la table [personnes] est le suivant :

Image non disponible

On voit en [6] que la 1ère insertion a été conservée. Cela vient du fait qu’elle a été faite au sein d’une transaction 1 et que l’erreur qui a suivi a été faite au sein d’une transaction 2.

19-3. Scripts 02 : les mappings de [sqlalchemy]

Image non disponible

Les scripts 02 sont une variante des scripts 01. On essaie de faire le maximum de configurations dans [config.py]. On y configure maintenant l’environnement [sqlalchemy] de l’application :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
def configure():
    # chemin absolu référence des chemins relatifs de la configuration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # chemins absolus des dépendances
    absolute_dependencies = [
        # BaseEntity, MyException, Personne, Utils
        f"{root_dir}/classes/02/entities",
    ]

    # on fixe le syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # imports
    from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
    from sqlalchemy.orm import mapper, sessionmaker

    # lien vers une base de données MySQL
    engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

    # metadata
    metadata = MetaData()

    # la table
    personnes_table = Table("personnes", metadata,
                            Column('id', Integer, primary_key=True),
                            Column('prenom'String(30), nullable=False),
                            Column("nom"String(30), nullable=False),
                            Column("age", Integer, nullable=False),
                            UniqueConstraint('nom''prenom', name='uix_1')
                            )

    # le mapping
    from Personne import Personne

    mapper(Personne, personnes_table, properties={
        'id': personnes_table.c.id,
        'prénom': personnes_table.c.prenom,
        'nom': personnes_table.c.nom,
        'âge': personnes_table.c.age
    })

    # la session factory
    Session = sessionmaker()
    Session.configure(bind=engine)

    # on met ces informations dans la config
    config = {}
    config["Session"= Session
    config["metadata"= metadata
    config["engine"= engine
    config["personnes_table"= personnes_table

    # configuration des classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # on rend la config
    return config

Commentaires

  • lignes 2-12 : configuration du Python Path ;

  • lignes 14-45 : on configure l’environnement [sqlalchemy] ;

  • lignes 47-52 : l’environnement [sqlalchemy] est mis dans le dictionnaire de la configuration ;

  • lignes 54-56 : on configure la classe [Personne] ;

Avec cette configuration le script [main] devient le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
# on configure l'application
import config

config = config.configure()

# le syspath est configuré - on fait les imports
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError

from Personne import Personne

session = None
try:
    # une session
    session = config["Session"]()

    # suppression de la table [personnes]
    session.execute("drop table if exists personnes")

    # recréation de la table à partir du mapping
    config["metadata"].create_all(config["engine"])

    # deux insertions
    session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
    personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
    session.add(personne)

    # validation des deux insertions
    session.commit()

    # une requête
    personnes = session.query(Personne).all()

    # affichage
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # deux autres insertions dont la seconde échoue
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))

    # une requête
    personnes = session.query(Personne).all()

    # affichage
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # validation de la session
    session.commit()

except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
    # affichage
    print(f"L'erreur suivante s'est produite : {erreur}")
    # annulation de la dernière session
    if session:
        print("rollback...")
        session.rollback()
finally:
    # affichage
    print("Travail terminé...")
    # on libère les ressources de la session
    if session:
        session.close()

Les résultats de l’exécution sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/02/main.py
Liste des personnes-----------
{"âge": 10, "nom": "y", "prénom": "x", "id": 1}
{"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
[SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
[parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Travail terminé...

Process finished with exit code 0

Dans phpMyAdmin, la table [personnes] est devenue la suivante :

Image non disponible

Maintenant, regardons la table [personnes] générée par [sqlalchemy] :

Image non disponible
  • en [6], les types utilisés pour les différentes colonnes ;

  • en [7], on voit que la colonne [id] a l’attribut [AUTO_INCREMENT]. Cela signifie que lors de l’insertion d’une ligne dans la table, si cette ligne n’a pas de valeur pour la colonne [id], celle-ci sera générée par MySQL de façon incrémentale : 1, 2, 3, … Cette propriété nous évite de nous préoccuper de la valeur de la clé primaire lorsque nous faisons une insertion dans la table : nous laissons MySQL la générer ;

  • en [8], on voit que la colonne [id] est clé primaire ;

  • en [9], on retrouve la contrainte d’unicité sur les champs [nom, prenom] ;

19-4. Scripts 03 : manipulation des entités de la session [sqlalchemy]

Image non disponible

Le fichier de configuration [config] est le même que dans l’exemple précédent. Dans le script [main] on fait les opérations classiques [INSERT, UPDATE, DELETE, SELECT] sur la table [personnes] à l’aide des méthodes de [sqlalchemy] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
# on configure l'application
import config

config = config.configure()

# imports
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne

# affiche le contenu de la table [personnes]
def affiche_table(session: Session):
    print("----------------")
    # une requête
    personnes = session.query(Personne).all()
    # affichage
    affiche_personnes(personnes)

# affiche une liste de personnes
def affiche_personnes(personnes: list):
    print("----------------")
    # affichage
    for personne in personnes:
        print(personne)


# main ---------------------------
session = None
try:
    # une session
    session = config["Session"]()

    # suppression de la table [personnes]
    # checkfirst=True : vérifie d'abord que la table existe
    config["personnes_table"].drop(config["engine"], checkfirst=True)

    # recréation de la table à partir du mapping
    config["metadata"].create_all(config["engine"])

    # des insertions
    session.add(Personne().fromdict({"prénom": "Pierre", "nom": "Nicazou", "âge": 35}))
    session.add(Personne().fromdict({"prénom": "Géraldine", "nom": "Colou", "âge": 26}))
    session.add(Personne().fromdict({"prénom": "Paulette", "nom": "Girondé", "âge": 56}))

    # on affiche le contenu de la session
    affiche_table(session)

    # liste des personnes par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # affichage
    affiche_personnes(personnes)

    # liste des personnes ayant un âge dans l'intervalle [20,40] par ordre décroissant de l'âge
    # puis à âge égal par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
    personnes = session.query(Personne). \
        filter(Personne.âge >= 20, Personne.âge <= 40). \
        order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())

    # affichage
    affiche_personnes(personnes)

    # insertion de mme Bruneau
    bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
    session.add(bruneau)
    # modification de son âge
    bruneau.âge = 47

    # liste des personnes ayant Bruneau pour nom
    personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()

    # affichage
    affiche_personnes([personne])

    # suppression de Mme Bruneau
    session.delete(personne)

    # liste des personnes ayant Bruneau pour nom
    personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")

    # affichage
    affiche_personnes(personnes)

    # validation de la session
    session.commit()

except (DatabaseError, InterfaceError, IntegrityError) as erreur:
    # affichage
    print(f"L'erreur suivante s'est produite : {erreur}")
    # annulation de la dernière session
    if session:
        session.rollback()

finally:
    # affichage
    print("Travail terminé...")
    # on libère les ressources de la session
    if session:
        session.close()

Commentaires

  • lignes 20-25 : la fonction [affiche_personnes] affiche les éléments d’une liste de personnes ;

  • lignes 12-18 : la fonction [affiche_table] affiche le contenu de la table [personnes] ;

  • lignes 34-36 : on supprime la table [personnes]. Contrairement aux versions précédentes, on n’utilise pas un ordre SQL mais une méthode de [sqlalchemy] :

    • config["personnes_table"] est l’objet [Table] décrivant la table [personnes] ;

    • config["engine"] est la chaîne de connexion à la base de données [dbpersonnes] ;

    • le paramètre nommé [checkfirst=True] demande à ce que l’opération ne se fasse que si la table [personnes] existe ;

  • lignes 38-39 : la table [personnes] est recréée ;

  • lignes 41-44 : trois personnes sont mises dans la session. On rappelle qu’elles ne sont pas forcément insérées immédiatement dans la table [personnes]. Cela dépend de la stratégie de [sqlalchemy] qui vise la performance ;

  • lignes 46-47 : le contenu de la table [personnes] es affiché. Si les insertions des trois personnes n’avaient pas encore été faites elles le sont maintenant à cause de cette demande ;

  • lignes 49-50 : un exemple d’utilisation de la méthode [order_by] qui permet de présenter les résultats d’une requête dans un certain ordre. La syntaxe [order_by(critère1, critère2)] affiche les résultats d’abord selon le critère [critère1] et lorsque des lignes présentent la même valeur de [critère1], elles sont alors triées selon le critère [critère2]. On peut mettre plusieurs critères ainsi ;

  • lignes 55-59 : introduisent la notion de filtre avec la méthode [filter]. La notation [filter(critère1, critère2)] fait un ET logique (AND) entre les critère utilisés ;

  • lignes 64-67 : une nouvelle personne est mise en session ;

  • lignes 70-71 : un autre exemple de requête filtrée. La fonction [func.lower(param)] rend [param] en minuscules. Il existe ainsi d’autres fonctions disponibles notées [func.xx]. Dans l’expression de la ligne 71 :

    • [session.query.filter] rend une liste d’objets [Personne] ;

    • [session.query.filter.first] rend le 1er élément de cette liste ;

  • ligne 77 : on supprime un élément de la session ;

  • ligne 86 : la session est validée ;

Les résultats de l’exécution sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/03/main.py
----------------
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0
  • lignes 4-6 : le contenu de la session ;

  • lignes 8-10 : le contenu de la session dans l’ordre décroissant des noms ;

  • lignes 12-13 : le contenu de la session pour les personnes dont l’âge est dans l’intervalle [20, 40] ;

  • ligne 15 : la personne de nom « bruneau » ;

Dans phpMyAdmin, le contenu de la table [personnes] à la fin de l’exécution est le suivant :

Image non disponible

19-5. Scripts 04 : utilisation d’une base [PostgreSQL]

Image non disponible

Le dossier [04] est une copie du dossier [03]. On change une unique chose, la chaîne de connexion dans le fichier [config] :

 
Sélectionnez
1.
2.
    # lien vers une base de données PostgreSQL
    engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")

Désormais cette chaîne de connexion désigne la base [dbpersonnes] d’un SGBD [PostgreSQL]. On notera l’utilisation du connecteur [psycopg2]. Il faut que celui-ci soit installé.

L’exécution du script [main] donne les résultats suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/04/main.py
----------------
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0

Avec l’outil [pgAdmin] (cf. paragraphe pgAdmin), la table [personnes] est dans l’état suivant :

Image non disponible

La table [personnes] a été générée avec le code SQL suivant :

Image non disponible
  • en [4-5], on voit que la colonne [id] est clé primaire. On voit également qu’elle a une valeur par défaut [mot clé DEFAULT] qui fait que si on insère une ligne sans clé primaire, celle-ci sera générée par le SGBD. C’est un fonctionnement fréquent : on laisse le SGBD générer les clés primaires ;

Cette version 05 des scripts [sqlalchemy] montre bien la facilité de passer d’un SGBD à l’autre : il a suffi de changer la chaîne de connexion dans un script de configuration. Rien d’autre n’a changé. Si on compare les types des colonnes [id, nom, prenom, age] ci-dessus avec ceux de la table MySQL de l’exemple 02, on voit qu’ils sont différents. [sqlalchemy] les adapte au SGBD utilisé. Cette facilité de s’adapter à un nouveau SGBD est une raison suffisante pour adopter [sqlalchemy] ou un autre ORM.

19-6. Scripts 05 : exemple complet

Image non disponible

L’exemple étudié est une reprise de celui étudié au paragraphe troiscouches-v01. Cet exemple présentait une architecture à trois couches [ui, métier, dao] qui manipulait des entités [Classe, Elève, Matière, Note]. Les entités étaient codées en dur dans une couche [dao]. Nous les mettons maintenant dans une base de données. Nous utiliserons deux SGBD : MySQL et PostgreSQL.

19-6-1. L’architecture de l’application

Image non disponible

L’architecture de l’application sera la suivante :

  • en [1-3], on trouve les couches [ui, métier, dao] déjà présentes dans l’exemple troiscouches-v01. La couche [dao] communique désormais avec la couche [ORM] ;

  • les couches [1-5] sont implémentées par du code Python ;

19-6-2. Les bases de données

Nous construisons une base MySQL nommée [dbecole] propriété de l’utilisateur [admecole] ayant le mot de passe [mdpecole]. Pour cela nous suivons la procédure décrite au paragraphe création d'une base de données :

Image non disponible
Image non disponible
Image non disponible
  • en [1], la base [dbecole] sans tables [3] ;

  • en [7], l’utilisateur [admecole] a tous les privilèges sur cette base de données ;

On fait de même avec le SGBD PostgreSQL. Nous construisons une base nommée [dbecole] propriété de l’utilisateur [admecole] ayant le mot de passe [mdpecole]. Pour cela nous suivons la procédure décrite au paragraphe création d'une base de données :

Image non disponible
  • en [1], la base [dbecole] ;

  • en [2], l’utilisateur [admecole] ;

  • en [3-4], la base [dbecole] est la propriété de l’utilisateur [admecole] ;

19-6-3. Les entités manipulées par l’application

Dans l’application troiscouches v01, les entités manipulées étaient les suivantes (cf. entités). Ce sont ces entités qui vont être stockées dans les bases de données précédentes. Nous ne dupliquerons pas ces entités dans la nouvelle application. Nous irons les chercher là où elles sont déjà définies.

La classe [Classe] :

 
Sélectionnez
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.
28.
29.
30.
# imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # attributs exclus de l'état de la classe
    excluded_keys = []

    # propriétés de la classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id : identifiant de la classe
        # nom : nom de la classe
        return BaseEntity.get_allowed_keys() + ["nom"]

    # getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    #  setters
    @nom.setter
    def nom(self: object, nom: str):
        # nom doit être une chaîne de caractères non vide
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

La classe [Elève] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
# imports
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # attributs exclus de l'état de la classe
    excluded_keys = []

    # propriétés de la classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id : identifiant de l'élève
        # nom : nom de l'élève
        # prénom : prénom de l'élève
        # classe : classe de l'élève
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # getters
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    #  setters
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # nom doit être une chaîne de caractères non vide
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        # prénom doit être une chaîne de caractères non vide
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            # on attend un type Classe
            if isinstance(value, Classe):
                self.__classe = value
            # ou un type dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # ou un type json
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

La classe [Matière] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
# imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # attributs exclus de l'état de la classe
    excluded_keys = []

    # propriétés de la classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id : identifiant de la matière
        # nom : nom de la matière
        # coefficient : coefficient de la matière
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    #  setters
    @nom.setter
    def nom(self: object, nom: str):
        # nom doit être une chaîne de caractères non vide
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        # le coefficient doit être un réel >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # erreur ?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

La classe [Note] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
# imports
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # attributs exclus de l'état de la classe
    excluded_keys = []

    # propriétés de la classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id : identifiant de la note
        # valeur : la note elle-même
        # élève : élève (de type Elève) concerné par la note
        # matière : matière (de type Matière) concernée par la note
        # l'objet Note est donc la note d'un élève dans une matière
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # getters
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # getters
    @valeur.setter
    def valeur(self: object, valeur: float):
        # la note doit être un réel entre 0 et 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # on attend un type Elève
            if isinstance(value, Elève):
                self.__élève = value
            # ou un type dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # ou un type json
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            # on attend un type Matière
            if isinstance(value, Matière):
                self.__matière = value
            # ou un type dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # ou un type json
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

19-6-4. Configuration

Image non disponible

La configuration a été éclatée sur plusieurs fichiers :

  • la configuration générale dans [config.py] : elle établit le Python Path de l’application et instancie les couches de l’architecture ;

  • la configuration de [sqlalchemy] dans [config_database] : elle fait les mappings Classes / Tables ;

  • les couches de l’application sont configurées dans [config_layers] ;

Le fichier [config] est le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
def configure(config: dict-> dict:
    import os

    # étape 1 ---
    # on établit le Python Path de l'application
    # chemin absolu du dossier de ce script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # chemin absolu référence des chemins relatifs de la configuration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # chemins absolus des dépendances
    absolute_dependencies = [
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # projet troiscouches v01
        f"{root_dir}/troiscouches/v01/interfaces",
        f"{root_dir}/troiscouches/v01/services",
        f"{root_dir}/troiscouches/v01/entities",
        # dossiers du présent projet
        script_dir,
        f"{script_dir}/../services",
    ]

    # mise à jour du syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # étape 2 ------
    # configuration base de données
    import config_database
    config = config_database.configure(config)

    # étape 3 ------
    # instanciation des couches de l'application
    import config_layers
    config = config_layers.configure(config)

    # on rend la config
    return config
  • lignes 4-27 : construction du Python Path de l’application ;

  • lignes 29-32 : configuration [sqlalchemy] ;

  • lignes 34-37 : configuration des couches de l’application ;

Le fichier [config_database] est le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
def configure(config: dict) -> dict:
    # config['sgbd'] est le nom du SGBD utilisé
    # mysql : MySQL
    # pgres : PostgreSQL

    # configuration sqlalchemy
    from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine

    from sqlalchemy.orm import mapper, relationship, sessionmaker

    # chaînes de connexion aux bases de données exploitées
    engines = {
        'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
        'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
    }
    # chaîne de connexion à la base de données exploitée
    engine = create_engine(engines[config['sgbd']])

    # metadata
    metadata = MetaData()

    # les tables de la base
    tables = {}
    # les classes mappées
    from Classe import Classe
    from Elève import Elève
    from Note import Note
    from Matière import Matière

    # la table des classes
    tables['classes'] = classes_table = \
        Table("classes", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              )

    mapper(Classe, tables['classes'], properties={
        'id': classes_table.c.id,
        'nom': classes_table.c.nom
    })

    # la table des élèves
    tables['élèves'] = élèves_table = \
        Table("élèves", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('prénom', String(30), nullable=False),
              # un élève appartient à une classe
              Column('classe_id', Integer, ForeignKey('classes.id')),
              )
    # mapping
    mapper(Elève, tables['élèves'], properties={
        'id': élèves_table.c.id,
        'nom': élèves_table.c.nom,
        'prénom': élèves_table.c.prénom,
        'classe': relationship(Classe, backref="élèves", lazy="select")
    })

    # la table des matières
    tables['matières'] = matières_table = \
        Table("matières", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('coefficient', Float, nullable=False)
              )
    # mapping
    mapper(Matière, tables['matières'], properties={
        'id': matières_table.c.id,
        'nom': matières_table.c.nom,
        "coefficient": matières_table.c.coefficient
    })

    # la table des notes
    tables['notes'] = notes_table = \
        Table("notes", metadata,
              Column('id', Integer, primary_key=True),
              Column('valeur', Float, nullable=False),
              # une note est celle d'un élève
              Column('élève_id', Integer, ForeignKey('élèves.id')),
              # une note est celle d'une matière
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # mapping
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valeur': notes_table.c.valeur,
        'élève': relationship(Elève, backref="notes", lazy="select"),
        'matière': relationship(Matière, backref="notes", lazy="select")
    })

    # configuration des entités [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

    # la session factory
    Session = sessionmaker()
    Session.configure(bind=engine)

    # une session
    session = Session()

    # on enregistre certaines informations dans le dictionnaire de la configuration
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # on rend la config
    return config

Commentaires

  • lignes 1-4 : la fonction [configure] reçoit un dictionnaire en paramètre. Seule la clé [sgbd] est exploitée. Elle vaut [mysql] si la base est une base MySQL, [pgres] si la base est une base PostgreSQL ;

  • lignes 6-9 : imports d’éléments de [sqlalchemy]. Le script [config_database] fait les mappings entre les tables de la base [dbecole] et les entités [Classes, Elève, Matière, Note]. Dans la table, les données de l’entité sont encapsulées dans une ligne. Dans le code Python, elles sont encapsulées dans un objet. D’où le nom ORM (Object Relational Mapper) : l’ORM fait un mapping (une liaison) entre les lignes d’une base de données relationnelle et des objets. Dans cette application, nous avons quatre entités [Classe, Elève, Matière, Note] qui seront reliées à quatre tables [classes, élèves, matières, notes]. Notez que les noms des tables peuvent comporter des caractères accentués ;

  • lignes 11-17 : la chaîne de connexion à la base de données exploitée. Celle-ci dépend de l’élément config['sgbd'] ;

  • lignes 24-28 : les entités de l’application qui vont faire l’objet d’un mapping [sqlalchemy]. Lorsque ces lignes seront exécutées, le Python Path aura déjà été établi par le script [config] ;

  • lignes 30-40 : le mapping entre l’entité [Classe] et la table [classes] ;

  • lignes 30-35 : la table [classes] est définie avec la classe [Table] de [sqlalchemy]. Nous indiquons que cette table a deux colonnes :

    • la colonne [id] qui est clé primaire et qui est le n° de la classe, ligne 33 ;

    • la colonne [nom] qui contient le nom de la classe, ligne 34 ;

  • lignes 31-32 : notez que la syntaxe x=y=z est légale en Python : la valeur de z est affectée à y puis la valeur de y à x ;

  • lignes 37-40 : on liste les correspondances entre les colonnes de la table [classes] et les propriétés de l’entité [Classe] ;

  • lignes 42-57 : le mapping entre l’entité [Elève] et la table [élèves] ;

  • lignes 51-57 : la table [élèves] est définie avec la classe [Table] de [sqlalchemy]. Nous indiquons que cette table a quatre colonnes :

    • la colonne [id] qui est clé primaire et qui est le n° de l’élève, ligne 45 ;

    • la colonne [nom] qui contient le nom de l’élève, ligne 46 ;

    • la colonne [prénom] qui contient le prénom de l’élève, ligne 47. Notez qu’un nom de colonne peut comporter des caractères accentués ;

    • ligne 49, la colonne [classe_id] qui contiendra le n° de la classe à laquelle appartient l’élève. On appelle cela une clé étrangère. [élèves.classe_id] est une clé étrangère (ForeignKey) sur la colonne [classes.id]. Cela signifie que la valeur de [élèves.classe_id] doit exister dans la colonne [classes.id] ;

  • lignes 51-57 : on liste les correspondances entre les colonnes de la table [élèves] et les propriétés de l’entité [Elève] :

    • les lignes 53-55 sont simples à comprendre ;

    • la ligne 56 est plus difficile : elle définit la valeur de la propriété [Elève.classe] comme étant calculée par la relation (relationship) de clé étrangère qui relie les tables [élèves] et [classes]. Les paramètres de la fonction [relationship] sont les suivants :

      • [Classe] : c’est le nom de l’entité avec laquelle l’entité [Elève] a une relation de clé étrangère. Celle-ci doit se matérialiser dans la table [élèves] par la présence d’une clé étrangère sur la table [classes]. Nous savons que celle-ci existe ;

      • [backref="élèves"] : le nom d’une propriété qui sera ajoutée à l’entité [Classe]. [Classe.élèves] sera la liste de tous les élèves de la classe. Cette propriété ne doit pas déjà exister. Si elle existe déjà, il faut simplement choisir un autre nom ici pour [backref]. Le développeur n’a pas à gérer cette propriété. C’est [sqlalchemy] qui le fera. Il doit simplement savoir qu’elle existe, ajoutée par [sqlalchemy], et qu’il peut l’utiliser dans son code ;

      • [lazy=’select’] : cela signifie que l’ORM ne doit pas chercher à donner immédiatement une valeur à la propriété [Elève.classe]. Il ne doit chercher sa valeur que lorsque le code la demande explicitement. Ainsi :

        • si le code demande la liste de tous les élèves, ceux-ci seront ramenés mais leur propriété [classe] ne sera pas calculée ;

        • un peu plus tard, le code s’intéresse à un élève [e] précis et référence sa classe [e.classe]. Cette référence va alors forcer [sqlalchemy] à faire une requête en base pour ramener la classe de l’élève, ceci de façon transparente pour le développeur ;

        • aussi mettre [lazy=’select’] vise à éviter les requêtes inutiles en base ;

    • ligne 56 : lorsque l’ORM récupère une ligne de la table [élèves], il récupère les informations [id, nom, prénom, classe_id]. A partir de là, il doit construire un objet Elève(id, nom, prénom, classe). Pour les propriétés [id, nom, prénom] ça ne pose pas de difficultés. Pour la propriété [classe], c’est plus compliqué. Sa valeur est une référence d’objet de type [Classe]. Or l’ORM ne dispose que d’une information [élèves.classe_id]. Comme [élèves.classe_id] est une clé étrangère sur la colonne [classes.id], on lui dit ici d’utiliser cette relation pour récupérer dans la table [classes] la ligne de n° id=[élèves.classe_id] (elle existe forcément) et de créer à partir de cette ligne l’objet [Classe] attendu par la propriété [Elève.classe] ;

  • lignes 59-71 : le mapping entre l’entité [Matière] et la table [matières] ;

  • lignes 59-65 : définition de la table [sqlalchemy] nommée [matières] ;

  • lignes 66-71 : on liste les correspondances entre les colonnes de la table [matières] et les propriétés de l’entité [Matière]. Il n’y a pas de difficultés ici ;

  • lignes 73-90 : le mapping entre l’entité [Note] et la table [notes] ;

  • lignes 73-82 : définition de la table [sqlalchemy] nommée [notes]. Elle a deux clés étrangères :

    • ligne 79, la colonne [notes.élève_id] prend ses valeurs dans la colonne [élèves.id] ]. Cette clé étrangère matérialise le fait qu’une note est la note d’un élève précis ;

    • ligne 81, la colonne [notes.matière_id] prend ses valeurs dans la colonne [matières.id]. Cette clé étrangère matérialise le fait qu’une note est une note dans une matière précise ;

  • lignes 84-90 : le mapping entre l’entité [Note] et la table [notes] :

    • ligne 88 : la propriété [Note.élève] doit avoir pour valeur une instance de type [Elève]. L’ORM n’a pour information dans la ligne de la table [notes] que la colonne [notes.élève_id] qui référence la colonne [élèves.id]. On dit ici d’utiliser cette relation de clé étrangère pour retrouver l’instance [Elève] dont on a la note. Par ailleurs [relationship(Elève, backref="notes", …)] va créer la nouvelle propriété [Elève.notes] qui sera la liste des notes de l’élève. Cette propriété ne doit pas déjà exister dans la classe [Elève] ;

    • ligne 89 : la propriété [Note.matière] doit avoir pour valeur une instance de type [Matière]. L’ORM n’a pour information dans la ligne de la table [notes] que la colonne [notes.matière_id] qui référence la colonne [matières.id]. On dit ici d’utiliser cette relation de clé étrangère pour retrouver l’instance [Matière] dont on a la note. Par ailleurs [relationship(Matière, backref="notes", …)] va créer la nouvelle propriété [Matière.notes] qui sera la liste des notes dans la matière. Cette propriété ne doit pas déjà exister dans la classe [Matière] ;

  • lignes 92-96 : on définit pour chaque entité dérivée de [BaseEntity] la liste des propriétés à exclure du dictionnaire des propriétés de l’entité (BaseEntity.asdict). Nous avons vu que [sqlalchemy] ajoutait la propriété [_sa_instance_state] à toutes entités mappées. On ne la veut pas dans le dictionnaire des propriétés. Par ailleurs on a vu que les mappings précédents avaient ajouté de nouvelles propriétés aux entités :

    • [Elève.notes] : toutes les notes de l’élève ;

    • [Classe.élèves] : tous les élèves de la classe ;

    • [Matière.notes] : toutes les notes de la matière ;

      En général, on ne veut pas ces propriétés ajoutées dans l’état de l’entité. En effet, calculer leur valeur a un coût SQL et cette valeur est souvent inutile. Ainsi si on récupère l’élève de nom ‘X’ :

    • l’ORM va ramener une entité [Elève(id, nom, prénom, classe, notes)]. A cause de [lazy=’select’], les propriétés [classe, notes] liées à des clés étrangères de la base n’auront pas été calculées ;

    • maintenant si j’affiche la chaîne jSON de cet élève, on sait que ce sera la chaîne jSON du dictionnaire [asdict] de l’entité. Si les propriétés [classe] et [notes] sont dedans, [sqlalchemy] va être obligé de faire des requêtes SQL pour calculer leurs valeurs. C’est coûteux. Si on peut éviter ces requêtes, c’est préférable ;

    • ici, nous avons exclu toutes les propriétés liées à une clé étrangère ;

  • lignes 98-100 : instanciation et configuration d’une [Session factory] (factory=usine de production). L’objet [Session] sert à créer des sessions [sqlalchemy] adossées à des transactions ;

  • lignes 102-103 : création d’une session sqlalchemy] ;

  • ligne 106 : certains éléments de la configuration [sqlalchemy] sont mis dans le dictionnaire global de la configuration de l’application ;

  • ligne 109 : on rend ce dictionnaire ;

Le fichier [config_layers] configure les couches de l’application :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
def configure(config: dict) -> dict:
    # instanciation couche [dao]
    from DatabaseDao import DatabaseDao
    dao = DatabaseDao(config)

    # instanciation de la couche [métier]
    from Métier import Métier
    métier = Métier(dao)

    # instanciation de la couche [ui]
    from Console import Console
    ui = Console(métier)

    # on met les couches dans la config
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # on rend la config
    return config
  • ligne 1 : la fonction [configure] reçoit le dictionnaire de la configuration globale de l’application ;

  • lignes 2-12 : les couches de l’application sont instanciées ;

  • lignes 15-17 : les références des couches sont mises dans la configuration globale ;

  • ligne 20 : on rend la nouvelle configuration ;

19-6-5. La couche [dao] - 1

Image non disponible

Il faut comprendre ici que la couche [dao] [3] communique avec l’ORM [sqlalchemy] [4] configuré comme il a été décrit dans le paragraphe précédent. Des trois couches [ui, métier, dao] de l’application troiscouches v01, seule la couche [dao] doit être réécrite. Les couches [ui, métier] sont conservées.

L’implémentation de la couche [dao] a été placée dans le dossier [services] :

Image non disponible

[InterfaceDatabaseDao] est l’interface de la couche [dao] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao


class InterfaceDatabaseDao(InterfaceDao, ABC):

    # initialisation de la base de données
    @abstractmethod
    def init_database(self, data: dict):
        pass
  • ligne 6 : l’interface [InterfaceDatabaseDao] dérive à la fois de la classe [ABC] pour être une classe abstraite et de l’interface [InterfaceDao] du projet troiscouches v01 ;

  • lignes 8-11 : on ajoute la méthode [init_database] aux méthodes héritées de [InterfaceDao]. Elle aura pour rôle d’initialiser la base de données avec les données du dictionnaire [data] qu’on lui passe en paramètre ligne 10 ;

Rappelons que l’interface [InterfaceDao] était la suivante :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
# imports
from abc import ABC, abstractmethod

# interface Dao
from Elève import Elève


class InterfaceDao(ABC):
    # liste des classes
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # liste des élèves
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # liste des matières
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # liste des notes
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # liste des notes d'un élève
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # chercher un élève par son id
    @abstractmethod
    def get_élève_by_id(self: object, élève_id: int) -> Elève:
        pass

L’implémentation de la couche [dao] est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        …
…
  • ligne 11 : la classe [DatabaseDao] implémente l’interface [InterfaceDatabaseDao] ;

  • lignes 13-16 : le constructeur de la classe. Il reçoit en paramètre, le dictionnaire de la configuration de l’application ;

  • ligne 15 : on mémorise la configuration [sqlalchemy] ;

  • ligne 16 : on mémorise la session [sqlalchemy] au travers de laquelle on va manipuler la base de données ;

  • ligne 18 : la méthode [init_database] initialise la base de données avec le dictionnaire [data] ;

Le dictionnaire [data] est implémenté par le script [data.py] suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
def configure():
    from Classe import Classe
    from Elève import Elève
    from Matière import Matière
    from Note import Note

    # on instancie les classes
    classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
    classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
    classes = [classe1, classe2]
    # les matières
    matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
    matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
    matières = [matière1, matière2]
    # les élèves
    élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
    élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
    élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
    élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
    élèves = [élève11, élève21, élève32, élève42]
    # les notes des élèves dans les différentes matières
    note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
    note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
    note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
    note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
    note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
    note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
    note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
    note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
    notes = [note1, note2, note3, note4, note5, note6, note7, note8]
    # on regroupe l'ensemble
    data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
    # on rend les données
    return data
  • ligne 34 : le dictionnaire qui sera passé à la méthode [init_database]. Ce dictionnaire est composé des clés suivantes (ligne 32) :

    • [élèves] : la liste des élèves ;

    • [classes] : la liste des classes ;

    • [matières] : la liste des matières ;

    • [notes] : la liste des notes de tous les élèves dans toutes les matières ;

Revenons à la méthode [init_database] :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
def init_database(self, data: dict):
        # config de la bd
        database = self.database
        engine = database['engine']
        metadata = database['metadata']
        tables = database['tables']

        try:
            # suppression des tables existantes
            # checkfirst=True : vérifie d'abord que la table existe
            tables["notes"].drop(engine, checkfirst=True)
            tables["matières"].drop(engine, checkfirst=True)
            tables["élèves"].drop(engine, checkfirst=True)
            tables["classes"].drop(engine, checkfirst=True)

            # recréation des tables à partir du mapping
            metadata.create_all(engine)

            # remplissage des tables
            session = self.session

            # classes
            classes = data["classes"]
            for classe in classes:
                session.add(classe)

            # matières
            matières = data["matières"]
            for matière in matières:
                session.add(matière)

            # élèves
            élèves = data["élèves"]
            for élève in élèves:
                session.add(élève)

            # notes
            notes = data["notes"]
            for note in notes:
                session.add(note)

            # commit
            session.commit()
        except (DatabaseError, InterfaceError, IntegrityError) as erreur:
            # annulation de la session
            if session:
                session.rollback()
            # on remonte l'exception
            raise MyException(23, f"{erreur}")
  • lignes 3-6 : on récupère des informations dans la configuration de la base de données ;

  • lignes 9-14 : nous avons vu que la configuration [sqlalchemy] avait mappé quatre entités sur quatre tables [élèves, matières, classes, notes]. On commence par supprimer ces tables si elles existent ;

  • lignes 16-17 : on recrée les quatre tables qu’on vient de supprimer ;

  • lignes 22-25 : on met toutes les classes dans la session ;

  • lignes 27-30 : on met toutes les matières dans la session ;

  • lignes 32-35 : on met tous les élèves dans la session ;

  • lignes 37-40 : on met toutes les notes dans la session ;

  • pour faire ces ajouts, on a suivi un ordre. On a commencé par les entités n’ayant pas de relations avec d’autres entités pour terminer par celles qui en avaient. Ainsi lorsqu’on ajoute les élèves dans la session, les classes que ceux-ci référencent sont déjà en session ;

  • ligne 43 : la session [sqlalchemy] est validée. Après cette opération, on est sûrs que toutes les données en session ont été synchronisées avec la base de données. En clair, elles sont arrivées dans les tables. Ceci a pu se faire grâce aux mappings qui ont été faits dans la configuration de [sqlalchemy]. [sqlalchemy] sait comment chaque entité doit être stockée dans les tables. [sqlalchemy] a également généré les clés étrangères que peuvent posséder les tables ;

  • lignes 44-49 : si on rencontre un problème, la session [sqlalchemy] est annulée et ligne 49, une exception est levée ;

19-6-6. Initialisation de la base de données

Image non disponible

Le script [main_init_database] initialise la base de données avec le contenu du script [data.py]. Son code est le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
# on attend un paramètre mysql ou pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# on configure l'application
import config
config = config.configure({'sgbd': sgbd})

# le syspath est configuré - on peut faire les imports
from MyException import MyException

# on récupère les données à mettre en base
import data
data = data.configure()

# on récupère la couche [dao]
dao = config["dao"]

# ----------- main
try:
    # création et initialisation des tables de la bd
    dao.init_database(data)
except MyException as ex:
    # on affiche l'erreur
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # libération des ressources mobilisées par l'application
    import shutdown
    shutdown.execute(config)
# fin
print("Travail terminé...")
  • lignes 1-11 : le script attend un paramètre [mysql] ou [pgres] selon qu’on veut initialiser une base MySQL ou PostgreSQL ;

  • lignes 13-15 : l’application est configurée pour le SGBD passé en paramètre ;

  • lignes 20-22 : on récupère les données à mettre en base ;

  • ligne 25 : la couche [dao] a déjà été instanciée et est accessible dans la configuration de l’application ;

  • ligne 30 : la base de données est initialisée ;

  • lignes 34-37 : erreur ou pas, on libère les ressources de l’application à l’aide du module [shutdown] ;

Le module [shutdown.py] est le suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
def execute(config: dict):
    # on libère les ressources mobilisées par l'application
    sqlalchemy_session = config['database']['session']
    if sqlalchemy_session:
        sqlalchemy_session.close()

La fonction [shutdown.execute] ferme la session [sqlalchemy] utilisée pour initialiser la base de données.

Nous créons une 1ère configuration d’exécution (cf. configuration d’exécution) pour exécuter [main_init_database] avec le SGBD MySQL :

Image non disponible

Les résultats de l’exécution de cette configuration sont les suivants dans phpMyAdmin :

Image non disponible
Image non disponible
Image non disponible

Pour le SGBD [PostgreSQL], nous utilisons la configuration d’exécution suivante :

Image non disponible

A l’exécution, les résultats dans [pgAdmin] sont les suivants :

Image non disponible
Image non disponible
Image non disponible

On notera la facilité avec laquelle on a pu changer de SGBD.

19-6-7. La couche [dao] – 2

Nous revenons sur la classe [DatabaseDao] qui implémente la couche [dao]. Nous n’avons montré pour l’instant que l’implémentation de la méthode [init_database]. Nous montrons maintenant l’implémentation des autres méthodes :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        …

    # liste de toutes les classes
    def get_classes(self: object) -> list:
        # requête
        return self.session.query(Classe).all()

    # liste de tous les élèves
    def get_élèves(self: object) -> list:
        # requête
        return self.session.query(Elève).all()

    # liste de toutes les matières
    def get_matières(self: object) -> list:
        # requête
        return self.session.query(Matière).all()

    # la liste des notes de tous les élèves
    def get_notes(self: object) -> list:
        # requête
        return self.session.query(Note).all()

    # la liste des notes d'un élève particulier
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # on cherche l'élève - une exception est lancée s'il n'existe pas
        # on la laisse remonter
        élève = self.get_élève_by_id(élève_id)
        # on récupère ses notes (lazy loading)
        notes = élève.notes
        # on rend un dictionnaire
        return {"élève": élève, "notes": notes}

    # un élève repéré par son n°
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # on cherche l'élève
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # a-t-on trouvé ?
        if élèves:
            return élèves[0]
        else:
            raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")

    # un élève repéré par son nom
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # on cherche l'élève
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # a-t-on trouvé ?
        if élèves:
            return élèves[0]
        else:
            raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")

    # une classe repérée par son n°
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # on cherche la classe
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # a-t-on trouvé ?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # une classe repérée par son nom
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # on cherche l'classe
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # a-t-on trouvé ?
        if classes:
            return classes[0]
        else:
            raise MyException(14, f"La classe de nom {classe_name} n'existe pas")

    # une matière repérée par son n°
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # on cherche l'matière
        matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
        # a-t-on trouvé ?
        if matières:
            return matières[0]
        else:
            raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")

    # une matière repérée par son nom
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # on cherche la matière
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # a-t-on trouvé ?
        if matières:
            return matières[0]
        else:
            raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
  • lignes 21-24 : la méthode [get_classes] doit rendre la liste des classes de l’école. Ligne 20, nous utilisons une requête déjà rencontrée ;

  • lignes 26-39 : trois autres méthodes similaires pour obtenir les listes des élèves, des matières et des notes ;

  • lignes 51-59 : la méthode [get_élève_by_id] doit rendre un élève identifié par son n°. Elle lance une exception si celui-ci n’existe pas ;

    • ligne 54 : on utilise une requête filtrée. On obtient une liste vide ou avec un élément ;

    • ligne 57 : si la liste récupérée n’est pas vide on rend le 1er élément de la liste ;

    • sinon ligne 59, on lance une exception ;

  • lignes 41-49 : la méthode [get_notes_for_élève_by_id] doit rendre les notes d’un élève identifié par son n° :

    • ligne 45, on utilise la méthode [get_élève_by_id] pour obtenir l’entité Elève de l’élève ;

    • ligne 47, on utilise la propriété [Elève.notes] créée par le mapping entre l’entité [Note] et la table [notes] (cf. paragraphe configuration sqlalchemy) et qui représente les notes de l’élève ;

    • ligne 49 : on rend un dictionnaire ;

  • lignes 61-109 : une série de méthodes analogues permettant de :

    • retrouver un élève par son nom, lignes 61-69 ;

    • retrouver une classe, lignes 71-89 ;

    • retrouver une matière, lignes 91-109 ;

19-6-8. Le script [main_joined_queries]

Image non disponible

Le script [main_joined_queries] s’appelle ainsi parce qu’il vise à mettre en lumière les requêtes faites implicitement par [sqlalchemy] pour récupérer des informations appartenant à plusieurs tables. Ces requêtes cachées au programmeur sont faites à chaque fois qu’une propriété d’une entité a été associée à la fonction [relationship] dans le mapping de l’entité. Par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
    # mapping
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valeur': notes_table.c.valeur,
        'élève': relationship(Elève, backref="notes", lazy="select"),
        'matière': relationship(Matière, backref="notes", lazy="select")
    })

Ci-dessus, le mapping entre l’entité [Note] et la table [notes] :

  • ligne 5, lorsque la propriété [élève] d’une entité [Note] est demandée pour la 1ère fois, elle sera cherchée dans la table [élèves] via une requête SQL. Tant que cette propriété n’a pas été demandée, elle reste indéfinie (lazy load). Une fois qu’elle a été obtenue, sa valeur reste en mémoire de l’ORM. Lorsqu’elle sera référencée une deuxième fois, l’ORM délivrera immédiatement sa valeur sans passer par une nouvelle requête SQL. Tout cela est transparent pour le développeur ;

  • idem pour la propriété inverse [Elève.notes] (backref), ligne 5 ;

  • idem pour la propriété [Note.matière] et sa propriété inverse [Matière.notes] (backref), ligne 6 ;

Le script [main_joined_queries] est le suivant :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
# on attend un paramètre mysql ou pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# on configure l'application
import config
config = config.configure({"sgbd": sgbd})

# le syspath est configuré - on peut faire els imports
from MyException import MyException

# la couche [dao]
dao = config["dao"]
try:
    # élève by id
    print("élève id=11 -----------")
    élève = dao.get_élève_by_id(11)
    print(f"élève={élève}")
    # la classe de l'élève (lazy loading)
    classe = élève.classe
    print(f"classe de l'élève : {classe}")
    # les élèves de la même classe (lazy loading)
    print("élèves dans la même classe :")
    for élève in classe.élèves:
        print(f"élève={élève}")

    # un élève par son nom
    print("élève nom='nom2' -----------")
    print(f"élève={dao.get_élève_by_name('nom2')}")
    # sa classe (lazy loading)
    print(f"classe de l'élève : {élève.classe}")

    # notes d'un élève
    print("notes de l'élève id=11 -----------")
    # d'abord l'élève
    élève = dao.get_élève_by_id(11)
    # puis ses notes (lazy loading)
    for note in élève.notes:
        # la note
        print(f"note={note}, "
              # la matière de la note (lazy loading)
              f"matière={note.matière}")

    # les élèves d'une classe
    print("élèves de la classe nom='classe1' -----------")
    # d'abord la classe
    classe = dao.get_classe_by_name('classe1')
    # puis les élèves (lazy loading)
    for élève in classe.élèves:
        print(élève)

    # même chose pour [classe2]
    print("élèves de la classe de nom 'classe2' -----------")
    classe = dao.get_classe_by_name('classe2')
    for élève in classe.élèves:
        print(élève)

    # les notes dans une matière
    print("matière de nom='matière1' -----------")
    # d'abord la matière
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # puis les notes dans cette matière (lazy loading)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # même chose pour matière2
    print("matière de nom='matière2' -----------")
    matière = dao.get_matière_by_name('matière2')
    print(f"matière={matière}")
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(f"note={note}")
except MyException as ex1:
    # on affiche l'erreur
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # on affiche l'erreur
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # on libère les ressources
    import shutdown
    shutdown.execute(config)

Les commentaires suffisent à comprendre le code.

On crée une configuration d’exécution pour MySQL :

Image non disponible

Les résultats de l’exécution sont les suivants :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
35.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_joined_queries.py mysql
élève id=11 -----------
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
classe de l'élève : {"nom": "classe1", "id": 1}
élèves dans la même classe :
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élève nom='nom2' -----------
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
classe de l'élève : {"nom": "classe1", "id": 1}
notes de l'élève id=11 -----------
note={"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}, matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}, matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
élèves de la classe nom='classe1' -----------
{"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
{"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élèves de la classe de nom 'classe2' -----------
{"classe_id": 2, "nom": "nom3", "prénom": "prénom3", "id": 32}
{"classe_id": 2, "nom": "nom4", "prénom": "prénom4", "id": 42}
matière de nom='matière1' -----------
matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
Notes dans la matière : 
{"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}
{"matière_id": 1, "valeur": 12.0, "élève_id": 21, "id": 2}
{"matière_id": 1, "valeur": 14.0, "élève_id": 32, "id": 3}
{"matière_id": 1, "valeur": 16.0, "élève_id": 42, "id": 4}
matière de nom='matière2' -----------
matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
Notes dans la matière : 
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}
note={"matière_id": 2, "valeur": 8.0, "élève_id": 21, "id": 6}
note={"matière_id": 2, "valeur": 10.0, "élève_id": 32, "id": 7}
note={"matière_id": 2, "valeur": 12.0, "élève_id": 42, "id": 8}

Process finished with exit code 0

Pour comprendre ces résultats, il faut se rappeler qu’on a exclu certaines propriétés du dictionnaire des entités (cf. configuration) :

 
Sélectionnez
1.
2.
3.
4.
5.
    # configuration des entités [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

Ainsi, lorsqu’on écrit [print(f"élève={élève}")] ligne 26 du code, la ligne 1 ci-dessus nous dit que les propriétés ['_sa_instance_state', 'notes', 'classe'] ne seront pas affichées. C’est ce qu’on voit ligne 3 des résultats. Toutes les autres propriétés sont affichées. Ainsi, toujours ligne 3, on découvre une nouvelle propriété [classe_id] qui n’existait initialement pas dans l’entité [Elève]. Cette propriété correspond directement à la colonne [classe_id] de la table [élèves]. Ainsi [sqlalchemy] a jouté les propriétés suivantes à l’entité [Elève] : [classe_id, _sa_instance_state, notes]. Il faut en avoir conscience, notamment parce qu’elles ne doivent pas déjà exister dans l’entité mappée.

Les propriétés exclues du dictionnaire des entités sont importantes. Si par exemple, on n’exclut pas les propriétés [notes, élève] de l’entité [Elève] alors l’opération [print(f"élève={élève}")] va les afficher et va donc, comme il vient d’être expliqué, provoquer des requêtes SQL implicites (lazy loading) pour récupérer les valeurs de ces propriétés. Si, comme ici, c’est une liste d’élèves qui est affichée, les opérations SQL implicites sont faites pour chaque élève. Cela peut être d’une part inutile et d’autre part sûrement coûteux en temps d’exécution.

Pour exécuter le script avec une base PostgreSQL, on crée la configuration d’exécution suivante :

Image non disponible

L’exécution donne les mêmes résultats qu’avec MySQL.

19-6-9. Le script [main_stats_for_élève]

Image non disponible

Le script [main_stats_for_élève] est celui déjà utilisé dans l’application troiscouches v01. Il s’appelait alors [main]. C’est une application console permettant d’obtenir certains indicateurs sur les notes d’un élève : [moyenne pondérée, min, max, liste]. Il s’insère dans l’architecture suivante :

Image non disponible

Dans cette architecture en couches, seule la couche [dao] a été changée entre l’application troiscouches v01 et celle-ci. Comme la nouvelle couche [dao] respecte l’interface [InterfaceDao] de l’ancienne couche [dao], les couches [ui, métier] n’ont pas à être changées. On peut donc continuer à utiliser celles définies dans l’application troiscouches v01.

Le script [main_stats_for_élève] implémente la couche [main] du schéma ci-dessus de la façon suivante :

 
Sélectionnez
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.
28.
29.
30.
31.
32.
33.
34.
# on attend un paramètre mysql ou pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# on configure l'application
import config
config = config.configure({'sgbd': sgbd})

# le syspath est configuré - on peut faire les imports
from MyException import MyException

# la couche [ui]
ui = config["ui"]
try:
    # exécution couche [ui]
    ui.run()
except MyException as ex1:
    # on affiche l'erreur
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # on affiche l'erreur
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # on libère les ressources
    import shutdown
    shutdown.execute(config)
  • ligne 20 : on récupère une référence sur la couche [ui] dans la configuration de l’application ;

  • ligne 24 : on lance le dialogue avec l’utilisateur à l’aide de l’unique méthode de la couche [ui] ;

Une configuration d’exécution pour PostgreSQL serait la suivante :

Image non disponible

Voici un exemple d’exécution avec cette configuration :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_stats_for_élève.py pgres
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"prénom": "prénom1", "id": 11, "classe_id": 1, "nom": "nom1"}, notes=[10.0 6.0], max=10.0, min=6.0, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[11, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *

Process finished with exit code 0

précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Serge Tahé et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2020 Developpez.com.