Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !


Jython est livré avec le module xzJDBC qui permet d'accéder à une base de données à partir d'une API (Application Programming Interface – L'ensemble des spécifications de classes, méthodes, fonctions, constantes, etc. qui permettent d'utiliser une technologie dans un programme.) compatible avec Python DB API 2.0. Néanmoins, il est aussi possible d'utiliser JDBC à partir de Jython pour accéder à une base de données. C'est utile, par exemple si vous êtes déjà familier de JDBC. Ainsi, vous pouvez accéder à la simplicité du langage Python – sans pour autant devoir apprendre une nouvelle API. Et c'est justement ce que nous allons faire ici.

Préparation

Avant toute chose, nous allons préparer la base. Ici, j'utiliserai PostgreSQL comme serveur de base de données.

La base qui va me servir de support est un embryon de base destinée à la traçabilité des produits de la pêche. Pour cet exemple, je vais me contenter de deux tables:

Espece
Qui regroupe les informations génériques sur les espèces suivies par notre système;
Prise
Qui stocke le nombre de prises (exprimées en nombre de caisse) de chacune des espèces pêchées au cours d'une campagne.

Note:

Si vous êtes familier de PostgreSQL, vous pourrez très vite passer sur cette partie.

La première étape va être de créer notre base, nos tables – et un utilisateur autorisé à y accéder:

sh# sudo -u postgres psql
Welcome to psql 8.3.7, the PostgreSQL interactive terminal.
[...]
postgres=# CREATE DATABASE tracabilite ENCODING 'UTF-8';
CREATE DATABASE
postgres=# \c tracabilite
You are now connected to database "tracabilite".

Piège:

Comme souvent, PostgreSQL (ou plutôt son driver JDBC) est malheureux avec les lettres accentuées dans les identifiants des champs, tables, bases de données, etc. C'est pourquoi, par exemple, la base se nomme tracabilite et non pas traçabilité.

tracabilite=# CREATE TABLE Espece (
tracabilite(#     id SERIAL PRIMARY KEY,
tracabilite(#     famille varchar(255) NOT NULL,
tracabilite(#     nom varchar(255) UNIQUE NOT NULL -- Nom vernaculaire
tracabilite(# );
NOTICE:  CREATE TABLE will create implicit sequence "espece_id_seq" for serial column "espece.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "espece_pkey" for table "espece"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "espece_nom_key" for table "espece"
CREATE TABLE
tracabilite=# CREATE INDEX Espece_famille_idx ON Espece(famille);
CREATE INDEX
tracabilite=# CREATE TABLE Prise (                            
tracabilite(#     campagne numeric(12) NOT NULL,
tracabilite(#     espece integer REFERENCES Espece(id),
tracabilite(#     quantite integer NOT NULL CHECK(quantite > 0)
tracabilite(# );
CREATE TABLE
tracabilite=# CREATE INDEX Prise_campagne_idx ON Prise(campagne);
CREATE INDEX
tracabilite=# CREATE ROLE gestion WITH LOGIN ENCRYPTED PASSWORD 'pass-1234';
CREATE ROLE
tracabilite=# GRANT SELECT, INSERT ON Espece TO gestion;
GRANT
tracabilite=# GRANT SELECT, INSERT ON Prise TO gestion;
GRANT
tracabilite=# INSERT INTO Espece (famille, nom)
tracabilite-# VALUES ('Scombridés', 'Maquereau'),
tracabilite-#        ('Scombridés', 'Thon Rouge'),
tracabilite-#        ('Scombridés', 'Thon Blanc'),
tracabilite-#        ('Scombridés', 'Espadon'),
tracabilite-#        ('Mulets', 'Mulet Capiton'),
tracabilite-#        ('Labridés', 'Vieille'),
tracabilite-#        ('Dorades', 'Pageau Commun'),
tracabilite-#        ('Motelles', 'Saint-Pierre'),
tracabilite-#        ('Motelles', 'Chinchard'),
tracabilite-#        ('Motelles', 'Bar');
INSERT 0 10

Et rajoutons maintenant le résultat des campagnes de pêche n°200905120001 et n°200905110001:

tracabilite=# INSERT INTO Prise (campagne, espece, quantite)
tracabilite-# VALUES (200905120001, 8, 8),
tracabilite-#        (200905120001, 10, 2);
INSERT 0 2
tracabilite=# INSERT INTO Prise (campagne, espece, quantite)
tracabilite-# VALUES (200905110001, 1, 12),
tracabilite-#        (200905110001, 10, 1),
tracabilite-#        (200905110001, 7, 3);
INSERT 0 3

Voilà: notre base est prête. Nous pouvons passer à des choses plus passionnantes en essayant de récupérer toutes ces informations à partir d'un programme Jython.

Utiliser JDBC dans Jython

Installation

Sous Debian vous aurez besoin du paquet libpg-java pour ce qui suit. Si ce logiciel n'est pas déjà sur votre machine utilisez le gestionnaire de paquets de votre distribution pour procéder à l'installation.

sh# apt-get install libpg-java

Tester l'accès au driver

Pour commencer très doucement, nous allons juste nous assurer que vous pouvez accéder au driver JDBC de PostgreSQL à partir de Jython. Cela impliquera sans doute de modifier le CLASSPATH:

sh$ export CLASSPATH=".:/usr/share/java/postgresql.jar"
sh$ jython
*sys-package-mgr*: processing new jar, '/usr/share/java/postgresql-jdbc3-8.2.jar'
Jython 2.5.0 (Release_2_5_0:6476, Jun 16 2009, 13:33:26) 
[Java HotSpot(TM) Client VM (Sun Microsystems Inc.)] on java1.6.0_12
Type "help", "copyright", "credits" or "license" for more information.
>>> from org.postgresql import Driver
>>>

Et oui: c'est tout! En effet, le fait que d'une part Jython signale le chargement d'un nouveau JAR, et que d'autre part, l'importation du driver PostgreSQL ne provoque aucune erreur signifie que le driver est bien accessible.

Remarque:

Si vous avez un message d'erreur, il y a de grandes chances que ce soit un problème de configuration du CLASSPATH. Vérifiez que vous avez bien indiqué l'emplacement de postgresql.jar – et accessoirement que l'archive de ce driver ne porte pas par hasard un autre nom comme postgresql-8.3.jar ou postgresql-jdbc3-8.3.jar.

Connexion à la base

Rentrons maintenant dans le vif du sujet. Nous allons nous connecter à la base tracabilite à partir de Jython:

# encoding=UTF-8
 
from java.sql import DriverManager
from org.postgresql import Driver
 
class DBHelper:
    def __init__(self, url, login, password):
        """Initialise la connexion avec la base de données.
        """
        self.conn = DriverManager.getConnection(url, login, password)
 
    def close(self):
        self.conn.close()
 
 
if __name__ == "__main__":
    dbHelper = DBHelper("jdbc:postgresql://localhost/tracabilite",
                        "gestion",
                        "pass-1234")
    try:
        """Le code ''utile'' prendra place ici"""
    finally:
        dbHelper.close()

Chargement du driver

Remarquez la manière dont est chargé le driver JDBC:

from org.postgresql import Driver

Cela s'oppose à la technique habituelle en Java:

Class.forName(org.postgresql.Driver)

Si vous voulez gérer le cas où le driver n'est pas trouvé, vous pouvez aussi inclure cet import dans un bloc try ... except:

try:
    from org.postgresql import Driver
except ImportError:
    # Ici, code des gestion de l'erreur
    # ...

Vous pouvez vérifier que le programme tourne sans erreur:

sh$ jython tracabilite.py
sh$ 

Si aucun message n'apparait, c'est que tout va bien. Dans le cas contraire, vérifiez le nom de la base, l'identifiant et le mot de passe de connexion. Assurez-vous aussi que votre serveur PostgreSQL est bien démarré.

Requête

Bon, il est temps de faire quelque chose de vraiment plus intéressant. Autrement dit, notre première requête. Ici, je vais donc ajouter une méthode doQuery à ma classe DBHelper. Et appeler cette méthode pour effectuer une recherche dans la base:

class DBHelper:
    # ...
    def doQuery(self, query):
	"""Exécute une requête.
	"""
	stmt = self.conn.createStatement()
	try:
	    rs = stmt.executeQuery(query)
	    while rs.next():
		print rs.getString(1) # Ici, je suppose que le ''ResultSet'' n'a qu'une colonne
	finally:
	    stmt.close()
 
if __name__ == "__main__":
    dbHelper = DBHelper("jdbc:postgresql://localhost/tracabilite",
                        "gestion",
                        "pass-1234")
    try:
        dbHelper.doQuery("""SELECT DISTINCT famille 
                                FROM Espece 
                                ORDER BY famille""")
    finally:
        dbHelper.close()

Remarque:

Observez le bloc try ... finally dans la méthode doQuery. Il permet de s'assurer que les ressources allouées par l'objet stmt sont bien libérées dans tous les cas – y compris si une exception est déclenchée lors du parcours du résultat.

sh$ jython tracabilite.py 
Dorades
Labridés
Motelles
Mulets
Scombridés

Utiliser une fonction de traitement

Pour l'instant, le traitement est codé en dur dans la méthode doQuery. Et il n'est guère utile, ni robuste d'ailleurs, puisqu'il se contente d'afficher le contenu de la première colonne du résultat.

Ce qui serait bien, c'est de pouvoir paramétrer le traitement à effectuer. C'est facile en Python, puisque les fonctions sont des objets, et peuvent donc être passées en argument.

Tout d'abord, ré-écrivons la méthode doQuery pour accepter une fonction de traitement:

# ...
 
    def doQuery(self, query, action):
        """Exécute une requête.
        """
        stmt = self.conn.createStatement()
        try:
            rs = stmt.executeQuery(query)
            while rs.next():
                action(rs)  # Appelle la fonction de traitement 
        finally:
            stmt.close()

Reste à définir la fonction de traitement et faire l'appel:

if __name__ == "__main__":
    dbHelper = DBHelper("jdbc:postgresql://localhost/tracabilite",
                        "gestion",
                        "pass-1234")
    try:
        # Définition de la fonction de traitement
        def prettyPrinter(rs):
            """Affichage ''raffiné'' de la liste des espèces.
            """
            print "*****", rs.getString(1).upper().center(20), "*****"
 
        # Appel à doQuery avec passage de la fonction de traitement en argument
        dbHelper.doQuery("""SELECT DISTINCT famille 
                                FROM Espece 
                                ORDER BY famille""",
                         prettyPrinter)
    finally:
        dbHelper.close()
sh$ jython tracabilite.py 
*****       DORADES        *****
*****       LABRIDÉS       *****
*****       MOTELLES       *****
*****        MULETS        *****
*****      SCOMBRIDÉS      *****

La technique présentée ci-dessus a deux avantages majeurs:

  1. Tout d'abord, la requête SQL et le traitement du résultat sont groupés en un seul et même site dans le code source. Ce qui améliore la compréhension du code et facilite les modifications ultérieures;
  2. Ensuite, cela permet de clairement séparer les responsabilités: la fonction de traitement s'occupe uniquement de la logique métier – alors que les détails techniques sont concentrés dans la méthode doQuery. Ici, en particulier, cette méthode s'assure de la libération des ressources allouées. Ce qui dispense l'auteur du code métier d'avoir à y penser ... ou de l'oublier.

Note:

Cette technique est bien entendu possible avec Java. Mais elle nécessite de définir une classe ou une interface et passer une instance de celle-ci en argument (via une inner class par exemple). Ce qui rend son usage moins élégant que dans un langage comme Python dans lequel les fonctions sont des objets.

Ceci dit, ce serait tout aussi vrai avec des langages qui supportent les fermetures, comme Groovy, Scala ou JavaScript. Mais ce n'est pas l'objet de ce tutoriel...

Histoire de prouver la souplesse de cette solution, imaginons que l'on veuille afficher le bilan des prises. Cela pourrait se faire en modifiant juste quelques lignes du programme – celles qui sont spécifiques au traitement à réaliser:

# ...
	def summaryPrinter(rs):
	    """Affichage du résumé des prises.
	    """
	    print "%20s: %2d" % (rs.getString("nom"), rs.getInt("quantite"))
 
	dbHelper.doQuery("""SELECT nom, SUM(quantite) AS quantite
                                FROM Espece INNER JOIN Prise
					    ON Espece.id = Prise.espece 
                                GROUP BY nom
				ORDER BY nom""",
			 summaryPrinter)
sh$ jython tracabilite.py 
                 Bar:  3
           Maquereau: 12
       Pageau Commun:  3
        Saint-Pierre:  8

Abstraction totale de JDBC

Peut-être que l'idée de passer le ResultSet en argument à la fonction de traitement vous semble en désaccord avec l'idée d'abstraire celle-ci des problèmes de mise en oeuvre de JDBC. Effectivement. Mais il est possible de faire mieux. En effet, la méthode doQuery peut être ré-écrite ainsi:

# ...
    def doQuery(self, query, action):
        """Exécute une requête.
        """
        stmt = self.conn.createStatement()
        try:
            rs = stmt.executeQuery(query)
 
            #
            # Récupère le nom des différentes colonnes du résultat
            metaData = rs.metaData
            columns = [metaData.getColumnName(i+1) for i in range(metaData.columnCount)]
 
            while rs.next():
                #
                # Extrait les données du ResultSet et les stocke dans un dictionnaire
                record = dict((col, rs.getObject(col)) for col in columns)
                action(record)
        finally:
            stmt.close()

La différence est que maintenant, c'est un dictionnaire qui est passé à la fonction de traitement. Plus un ResultSet. Ainsi, le traitement est totalement indépendant de JDBC.

getObject

Remarquez comment les colonnes sont extraites:

               record = dict((col, rs.getObject(col)) for col in columns)

La méthode getObject permet de récupérer un objet (Java) dont le type correspond à celui de la colonne (Long pour une colonne LONG, String pour une colonne VARCHAR, etc.).

Par la suite, comme Jython implémente l'auto-boxing, il est capable de convertir au besoin ces objets Java en leur type Python équivalent. C'est pourquoi aucune conversion explicite n'est nécessaire.

Après cette modification, la fonction de traitement peut être simplifiée:

# ...
        def summaryPrinter(record):
            """Affichage du résumé des prises.
            """
            print "%20s: %2d" % (record["nom"], record["quantite"])

Non seulement la fonction est un peu plus lisible ainsi, mais surtout, il devient possible de la tester et/ou de l'utiliser indépendamment de JDBC.

Générateur

Notre exploration de l'utilisation de JDBC sauce Python arrive à sa fin. Cependant, avant de terminer nous allons exploiter les générateurs introduits avec Python 2.3 pour simplifier le parcours du résultat de la requête.

En effet, l'idiome JDBC pour parcourir un ResultSet est le suivant:

# ...
            rs = stmt.executeQuery(query)
# ...
            while rs.next():
                # faire quelque-chose avec l'enregistrement actuel

Quel dommage que les ResultSet ne soient pas itérables! Ce qui nous permettrait d'écrire plus simplement:

# ...
            for record in stmt.executeQuery(query):
                # faire quelque-chose avec l'enregistrement

C'est quasiment ce que vont nous permettre les générateurs: définir notre propre itérateur. Et en plus, ça n'est pas trop compliqué. Tout d'abord je vais définir le générateur sous la forme d'une fonction globale:

def ResultSetIterator(rs):
    """Générateur permettant de parcourir les enregistrements 
    d'un ResultSet JDBC.
    """
    #
    # Récupère le nom des différentes colonnes du résultat
    metaData = rs.metaData
    columns = [metaData.getColumnName(i+1) for i in range(metaData.columnCount)]
 
    while rs.next():
        #
        # Extrait les données du ResultSet et les stocke dans un dictionnaire
        record = dict((col, rs.getObject(col)) for col in columns)
        yield record

Remarque:

Oui, vous avez bien vu, c'est un simple copier-coller du code que nous avions précédemment dans doQuery. La seule différence – et ce qui transforme cette fonction en générateur – c'est l'instruction yield.

Si vous n'êtes pas familier avec la notion de générateur, je ne peux que vous engager à lire la documentation de Python. Néanmoins, en deux mots, yield interrompt l'exécution de la fonction et renvoie une valeur. Seulement, à la différence d'un return, l'exécution de la fonction reprendra au point où elle s'était arrêtée lors de la prochaine itération.

Maintenant que nous pouvons parcourir un ResultSet à l'aide de notre tout nouveau générateur, la méthode doQuery se simplifie largement:

# ...
    def doQuery(self, query, action):
        """Exécute une requête.
        """
        stmt = self.conn.createStatement()
        try:
            for record in ResultSetIterator(stmt.executeQuery(query)):
                action(record)
        finally:
            stmt.close()

Conclusion

Au premier abord, JDBC ne semble pas être une des bibliothèques Java qui peut bénéficier le plus d'une utilisation de Jython. Pourtant, à l'usage il apparaît que, grâce à des constructions comme les compréhensions de liste ou les générateurs, le code peut être simplifié pour devenir nettement plus lisible que sa contre-partie Java.

Ainsi, en une soixantaine de ligne, nous avons réussi à obtenir un cadre générique et parfaitement réutilisable – puisque la fonction ResultSetIterator et la classe DBHelper sont totalement indépendantes de la base sous-jascente – qui permet d'accéder à une base de données avec JDBC.

A nouveau, si Jython ne s'avérerait pas nécessairement le meilleur choix pour la boucle critique d'une application, pour le prototypage rapide, ou lorsqu'il est important de pouvoir modifier facilement le code, il se révèle d'un emploi bien pratique.