Vous savez désormais comment vous connecter à une BDD depuis Java. Je vous ai montré comment lire et modifier des données. Après vous avoir fait découvrir tout cela, je me suis dit que montrer une approche un peu plus objet ne serait pas du luxe. C'est vrai, établir sans arrêt la connexion à notre base de données commence à être fastidieux. Je vous propose donc d'y remédier avec ce chapitre en découvrant le pattern singleton.
Pourquoi ne se connecter qu'une seule fois ?
Pourquoi veux-tu absolument qu'on ait une seule instance de notre objet Connection
?
Parce que cela ne sert pas à grand-chose de réinitialiser la connexion à votre BDD. Rappelez-vous que la connexion sert à établir le pont entre votre base et votre application. Pourquoi voulez-vous que votre application se connecte à chaque fois à la BDD ? Une fois la connexion effective, pourquoi vouloir l'établir de nouveau ? Votre application et votre BDD peuvent discuter !
Bon, c'est vrai qu'avec du recul, cela paraît superflu… Du coup, comment fais-tu pour garantir qu'une seule instance de Connection
existe dans l'application ?
C'est ici que le pattern singleton intervient ! Ce pattern est peut-être l'un des plus simples à comprendre même, s'il contient un point qui va vous faire bondir : le principe de base est d'interdire l'instanciation d'une classe, grâce à un constructeur déclaré private
.
Le pattern singleton
Nous voulons qu'il soit impossible de créer plus d'un objet de connexion. Voici une classe qui permet de s'assurer que c'est le cas :
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 | //CTRL + SHIFT + O pour générer les imports public class SdzConnection{ //URL de connexion private String url = "jdbc:postgresql://localhost:5432/Ecole"; //Nom du user private String user = "postgres"; //Mot de passe de l'utilisateur private String passwd = "postgres"; //Objet Connection private static Connection connect; //Constructeur privé private SdzConnection(){ try { connect = DriverManager.getConnection(url, user, passwd); } catch (SQLException e) { e.printStackTrace(); } } //Méthode qui va nous retourner notre instance et la créer si elle n'existe pas public static Connection getInstance(){ if(connect == null){ new SdzConnection(); } return connect; } } |
Nous avons ici une classe avec un constructeur privé : du coup, impossible d'instancier cet objet et d'accéder à ses attributs, puisqu'ils sont déclarés private
! Notre objet Connection
est instancié dans le constructeur privé et la seule méthode accessible depuis l'extérieur de la classe est getInstance()
. C'est donc cette méthode qui a pour rôle de créer la connexion si elle n'existe pas, et seulement dans ce cas.
Pour en être bien sûrs, nous allons faire un petit test… Voici le code un peu modifié de la méthode getInstance()
:
1 2 3 4 5 6 7 8 9 10 | public static Connection getInstance(){ if(connect == null){ new SdzConnection(); System.out.println("INSTANCIATION DE LA CONNEXION SQL ! "); } else{ System.out.println("CONNEXION SQL EXISTANTE ! "); } return connect; } |
Cela nous montre quand la connexion est réellement créée. Ensuite, il ne nous manque plus qu'un code de test. Oh ! Ben ça alors ! J'en ai un sous la main :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //CTRL + SHIFT + O pour générer les imports public class Test { public static void main(String[] args) { try { //Nous appelons quatre fois la méthode getInstance() PreparedStatement prepare = SdzConnection.getInstance().prepareStatement("SELECT * FROM classe WHERE cls_nom = ?"); Statement state = SdzConnection.getInstance().createStatement(); SdzConnection.getInstance().setAutoCommit(false); DatabaseMetaData meta = SdzConnection.getInstance().getMetaData(); } catch (SQLException e) { e.printStackTrace(); } } } |
La méthode en question est appelée quatre fois. Que croyez-vous que ce code va afficher ? Quelque chose comme la figure suivante !
Vous avez la preuve que l'instanciation ne se fait qu'une seule fois et donc que notre connexion à la BDD est unique ! La classe SdzConnection
peut être un peu simplifiée :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //CTRL + SHIFT + O pour générer les imports public class SdzConnection{ private static String url = "jdbc:postgresql://localhost:5432/Ecole"; private static String user = "postgres"; private static String passwd = "postgres"; private static Connection connect; public static Connection getInstance(){ if(connect == null){ try { connect = DriverManager.getConnection(url, user, passwd); } catch (SQLException e) { e.printStackTrace(); } } return connect; } } |
Attention toutefois, vous devrez rajouter la déclaration static
à vos paramètres de connexion.
Vous pouvez relancer le code de test, vous verrez qu'il fonctionne toujours ! J'avais commencé par insérer un constructeur privé car vous deviez savoir que cela existait, mais remarquez que c'était superflu dans notre cas…
Par contre, dans une application multithreads, pour être sûrs d'éviter les conflits, il vous suffit de synchroniser la méthode getInstance()
et le tour est joué. Mais - parce qu'il y a un mais - cette méthode ne règle le problème qu'avant l'instanciation de la connexion. Autrement dit, une fois la connexion instanciée, la synchronisation ne sert plus à rien.
Le problème du multithreading ne se pose pas vraiment pour une connexion à une BDD puisque ce singleton sert surtout de passerelle entre votre BDD et votre application. Cependant, il peut exister d'autres objets que des connexions SQL qui ne doivent être instanciés qu'une fois ; tous ne sont pas aussi laxistes concernant le multithreading.
Voyons donc comment parfaire ce pattern avec un exemple autre qu'une connexion SQL.
Le singleton dans tous ses états
Nous allons travailler avec un autre exemple et vu que j'étais très inspiré, revoici notre superbe singleton :
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 | public class SdzSingleton { //Le singleton private static SdzSingleton single; //Variable d'instance private String name = ""; //Constructeur privé private SdzSingleton(){ this.name = "Mon singleton"; System.out.println("\t\tCRÉATION DE L'INSTANCE ! ! !"); } //Méthode d'accès au singleton public static SdzSingleton getInstance(){ if(single == null) single = new SdzSingleton(); return single; } //Accesseur public String getName(){ return this.name; } } |
Ce n'est pas que je manquais d'inspiration, c'est juste qu'avec une classe toute simple, on comprend mieux les choses… Et voici notre classe de test :
1 2 3 4 5 6 | public class TestSingleton { public static void main(String[] args) { for(int i = 1; i < 4; i++) System.out.println("Appel N° " + i + " : " + SdzSingleton.getInstance().getName()); } } |
Cela nous donne la figure suivante.
La politique du singleton est toujours bonne. Mais je vais vous poser une question : quand croyez-vous que la création d'une instance soit la plus judicieuse ? Ici, nous avons exécuté notre code et l'instance a été créée lorsqu'on l'a demandée pour la première fois ! C'est le principal problème que posent le singleton et le multithreading : la première instance… Une fois celle-ci créée, les problèmes se font plus rares.
Pour limiter les ennuis, nous allons donc laisser cette lourde tâche à la JVM, dès le chargement de la classe, en instanciant notre singleton lors de sa déclaration :
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 | public class SdzSingleton { //Le singleton private static SdzSingleton single = new SdzSingleton(); //Variable d'instance private String name = ""; //Constructeur privé private SdzSingleton(){ this.name = "Mon singleton"; System.out.println("\t\tCRÉATION DE L'INSTANCE ! ! !"); } //Méthode d'accès au singleton public static SdzSingleton getInstance(){ if(single == null) single = new SdzSingleton(); return single; } //Accesseur public String getName(){ return this.name; } } |
Avec ce code, c'est la machine virtuelle qui s'occupe de charger l'instance du singleton, bien avant que n'importe quel thread vienne taquiner la méthode getInstance()
…
Il existe une autre méthode permettant de faire cela, mais elle ne fonctionne parfaitement que depuis le JDK 1.5. On appelle cette méthode « le verrouillage à double vérification ». Elle consiste à utiliser le mot clé volatile
combiné au mot clé synchronized
.
Pour les lecteurs qui l'ignorent, déclarer une variable volatile
permet d'assurer un accès ordonné des threads à une variable (plusieurs threads peuvent accéder à cette variable), marquant ainsi le premier point de verrouillage. Ensuite, la double vérification s'effectue dans la méthode getInstance()
: on effectue la synchronisation uniquement lorsque le singleton n'est pas créé.
Voici ce que cela nous donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class SdzSingleton { private volatile static SdzSingleton single; private String name = ""; private SdzSingleton(){ this.name = "Mon singleton"; System.out.println("\n\t\tCRÉATION DE L'INSTANCE ! ! !"); } public static SdzSingleton getInstance(){ if(single == null){ synchronized(SdzSingleton.class){ if(single == null) single = new SdzSingleton(); } } return single; } public String getName(){ return this.name; } } |
- Pour économiser les ressources, vous ne devriez créer qu'un seul objet de connexion.
- Le pattern singleton permet de disposer d'une instance unique d'un objet.
- Ce pattern repose sur un constructeur privé associé à une méthode retournant l'instance créée dans la classe elle-même.
- Afin de pallier au problème du multithreading, il vous suffit d'utiliser le mot clé
synchronized
dans la déclaration de votre méthode de récupération de l'instance, mais cette synchronisation n'est utile qu'une fois. À la place, vous pouvez instancier l'objet au chargement de la classe par la JVM, avant tout appel à celle-ci.