web-dev-qa-db-fra.com

Comment implémenter un gestionnaire DAO à l'aide de JDBC et de pools de connexions?

Mon problème est le suivant. J'ai besoin d'une classe qui fonctionne comme un point unique vers une connexion à une base de données dans un système Web, afin d'éviter d'avoir un utilisateur avec deux connexions ouvertes. J'en ai besoin pour être aussi optimal que possible et il devrait gérer chaque transaction dans le système. En d'autres termes, seule cette classe devrait pouvoir instancier des DAO. Et pour le rendre meilleur, il devrait également utiliser le pool de connexions! Que devrais-je faire?

23
Carlos Vergara

Vous devrez implémenter un DAO Manager . J'ai pris l'idée principale de ce site Web , mais j'ai fait ma propre implémentation qui résout quelques problèmes.

Étape 1: mise en commun des connexions

Tout d'abord, vous devrez configurer un pool de connexions . Un pool de connexions est, enfin, un pool de connexions. Lorsque votre application s'exécute, le pool de connexions démarre une certaine quantité de connexions, ceci est fait pour éviter de créer des connexions en runtime car c'est une opération coûteuse. Ce guide n'est pas destiné à expliquer comment en configurer un, alors allez y voir.

Pour mémoire, je vais utiliser Java comme langue et Glassfish comme mon serveur.

Étape 2: se connecter à la base de données

Commençons par créer une classe DAOManager. Donnons-lui des méthodes pour ouvrir et fermer une connexion en runtime. Rien d'extraordinaire.

public class DAOManager {

    public DAOManager() throws Exception {
        try
        {
            InitialContext ctx = new InitialContext();
            this.src = (DataSource)ctx.lookup("jndi/MYSQL"); //The string should be the same name you're giving to your JNDI in Glassfish.
        }
        catch(Exception e) { throw e; }
    }

    public void open() throws SQLException {
        try
        {
            if(this.con==null || !this.con.isOpen())
                this.con = src.getConnection();
        }
        catch(SQLException e) { throw e; }
    }

    public void close() throws SQLException {
        try
        {
            if(this.con!=null && this.con.isOpen())
                this.con.close();
        }
        catch(SQLException e) { throw e; }
    }

    //Private
    private DataSource src;
    private Connection con;

}

Ce n'est pas une classe très chic, mais ce sera la base de ce que nous allons faire. Donc, en faisant ceci:

DAOManager mngr = new DAOManager();
mngr.open();
mngr.close();

devrait ouvrir et fermer votre connexion à la base de données dans un objet.

Étape 3: Faites-en un seul point!

Et maintenant, si on faisait ça?

DAOManager mngr1 = new DAOManager();
DAOManager mngr2 = new DAOManager();
mngr1.open();
mngr2.open();

Certains pourraient argumenter, "pourquoi diable voudriez-vous faire ça?". Mais alors vous ne savez jamais ce que fera un programmeur. Même alors, le programmeur peut se tromper en fermant une connexion avant d'en ouvrir une nouvelle. De plus, c'est un gaspillage de ressources pour l'application. Arrêtez-vous ici si vous voulez réellement avoir deux ou plusieurs connexions ouvertes, ce sera une implémentation pour une connexion par utilisateur.

Afin d'en faire un seul point, nous devrons convertir cette classe en un singleton . Un singleton est un modèle de conception qui nous permet d'avoir une et une seule instance d'un objet donné. Alors, faisons-en un singleton!

  • Nous devons convertir notre constructeur public en constructeur privé. Nous devons seulement donner un exemple à celui qui l'appelle. Le DAOManager devient alors une usine!
  • Nous devons également ajouter une nouvelle classe private qui stockera réellement un singleton.
  • Parallèlement à tout cela, nous avons également besoin d'une méthode getInstance() qui nous donnera une instance singleton que nous pouvons appeler.

Voyons comment cela est mis en œuvre.

public class DAOManager {

    public static DAOManager getInstance() {
        return DAOManagerSingleton.INSTANCE;
    }  

    public void open() throws SQLException {
        try
        {
            if(this.con==null || !this.con.isOpen())
                this.con = src.getConnection();
        }
        catch(SQLException e) { throw e; }
    }

    public void close() throws SQLException {
        try
        {
            if(this.con!=null && this.con.isOpen())
                this.con.close();
        }
        catch(SQLException e) { throw e; }
    }

    //Private
    private DataSource src;
    private Connection con;

    private DAOManager() throws Exception {
        try
        {
            InitialContext ctx = new InitialContext();
            this.src = (DataSource)ctx.lookup("jndi/MYSQL");
        }
        catch(Exception e) { throw e; }
    }

    private static class DAOManagerSingleton {

        public static final DAOManager INSTANCE;
        static
        {
            DAOManager dm;
            try
            {
                dm = new DAOManager();
            }
            catch(Exception e)
                dm = null;
            INSTANCE = dm;
        }        

    }

}

Lorsque l'application démarre, chaque fois que quelqu'un a besoin d'un singleton, le système instanciera un DAOManager. Assez soigné, nous avons créé un point d'accès unique!

Mais singleton est un contre-motif pour des raisons ! Je sais que certaines personnes n'aimeront pas singleton. Cependant, cela résout le problème (et a résolu le mien) assez décemment. Ceci est juste un moyen de mettre en œuvre cette solution, si vous avez d'autres façons de le suggérer.

Étape 4: Mais il y a quelque chose qui ne va pas ...

Oui, bien sûr. Un singleton ne créera qu'une seule instance pour toute l'application! Et c'est faux à plusieurs niveaux, surtout si nous avons un système web où notre application sera multithread ! Comment pouvons-nous résoudre cela, alors?

Java fournit une classe nommée ThreadLocal. Une variable ThreadLocal aura une instance par thread. Hé, ça résout notre problème! En savoir plus sur son fonctionnement , vous devrez comprendre son objectif afin que nous puissions continuer.

Faisons alors notre INSTANCEThreadLocal. Modifiez la classe de cette façon:

public class DAOManager {

    public static DAOManager getInstance() {
        return DAOManagerSingleton.INSTANCE.get();
    }  

    public void open() throws SQLException {
        try
        {
            if(this.con==null || !this.con.isOpen())
                this.con = src.getConnection();
        }
        catch(SQLException e) { throw e; }
    }

    public void close() throws SQLException {
        try
        {
            if(this.con!=null && this.con.isOpen())
                this.con.close();
        }
        catch(SQLException e) { throw e; }
    }

    //Private
    private DataSource src;
    private Connection con;

    private DAOManager() throws Exception {
        try
        {
            InitialContext ctx = new InitialContext();
            this.src = (DataSource)ctx.lookup("jndi/MYSQL");
        }
        catch(Exception e) { throw e; }
    }

    private static class DAOManagerSingleton {

        public static final ThreadLocal<DAOManager> INSTANCE;
        static
        {
            ThreadLocal<DAOManager> dm;
            try
            {
                dm = new ThreadLocal<DAOManager>(){
                    @Override
                    protected DAOManager initialValue() {
                        try
                        {
                            return new DAOManager();
                        }
                        catch(Exception e)
                        {
                            return null;
                        }
                    }
                };
            }
            catch(Exception e)
                dm = null;
            INSTANCE = dm;
        }        

    }

}

J'aimerais vraiment ne pas faire ça

catch(Exception e)
{
    return null;
}

mais initialValue() ne peut pas lever d'exception. Oh, initialValue() tu veux dire? Cette méthode nous indiquera quelle valeur la variable ThreadLocal contiendra. Fondamentalement, nous l'initialisons. Ainsi, grâce à cela, nous pouvons maintenant avoir une instance par thread.

Étape 5: créer un DAO

Un DAOManager n'est rien sans un DAO. Nous devons donc au moins en créer deux.

Un DAO, abréviation de "Data Access Object" est un modèle de conception qui confie la responsabilité de la gestion des opérations de base de données à une classe représentant une certaine table.

Afin d'utiliser notre DAOManager plus efficacement, nous définirons un GenericDAO, qui est un DAO abstrait qui contiendra les opérations communes entre tous les DAO.

public abstract class GenericDAO<T> {

    public abstract int count() throws SQLException; 

    //Protected
    protected final String tableName;
    protected Connection con;

    protected GenericDAO(Connection con, String tableName) {
        this.tableName = tableName;
        this.con = con;
    }

}

Pour l'instant, ce sera suffisant. Créons des DAO. Supposons que nous ayons deux POJO: First et Second, tous deux avec juste un champ String nommé data et ses getters et setters.

public class FirstDAO extends GenericDAO<First> {

    public FirstDAO(Connection con) {
        super(con, TABLENAME);
    }

    @Override
    public int count() throws SQLException {
        String query = "SELECT COUNT(*) AS count FROM "+this.tableName;
        PreparedStatement counter;
        try
        {
        counter = this.con.PrepareStatement(query);
        ResultSet res = counter.executeQuery();
        res.next();
        return res.getInt("count");
        }
        catch(SQLException e){ throw e; }
    }

   //Private
   private final static String TABLENAME = "FIRST";

}

SecondDAO aura plus ou moins la même structure, en changeant simplement TABLENAME en "SECOND".

Étape 6: faire du directeur une usine

DAOManager non seulement devrait servir à servir de point de connexion unique. En fait, DAOManager devrait répondre à cette question:

Qui est responsable de la gestion des connexions à la base de données?

Les DAO individuels ne doivent pas les gérer, mais DAOManager. Nous avons répondu partiellement à la question, mais maintenant, nous ne devons laisser personne gérer d'autres connexions à la base de données, pas même les DAO. Mais, les DAO ont besoin d'une connexion à la base de données! Qui devrait le fournir? DAOManager en effet! Ce que nous devons faire est de créer une méthode d'usine dans DAOManager. Non seulement cela, mais DAOManager leur remettra également la connexion actuelle!

Factory est un modèle de conception qui nous permettra de créer des instances d'une certaine superclasse, sans savoir exactement quelle classe enfant sera renvoyée.

Commençons par créer un enum listant nos tables.

public enum Table { FIRST, SECOND }

Et maintenant, la méthode d'usine dans DAOManager:

public GenericDAO getDAO(Table t) throws SQLException 
{

    try
    {
        if(this.con == null || this.con.isClosed()) //Let's ensure our connection is open   
            this.open();
    }
    catch(SQLException e){ throw e; }

    switch(t)
    {
    case FIRST:
        return new FirstDAO(this.con);
    case SECOND:
        return new SecondDAO(this.con);
    default:
        throw new SQLException("Trying to link to an unexistant table.");
    }

}

Étape 7: Tout assembler

Nous sommes prêts à partir maintenant. Essayez le code suivant:

DAOManager dao = DAOManager.getInstance();
FirstDAO fDao = (FirstDAO)dao.getDAO(Table.FIRST);
SecondDAO sDao = (SecondDAO)dao.getDAO(Table.SECOND);
System.out.println(fDao.count());
System.out.println(sDao.count());
dao.close();

N'est-ce pas chic et facile à lire? Non seulement cela, mais lorsque vous appelez close(), vous fermez chaque connexion que les DAO utilisent. Mais comment? ! Eh bien, ils partagent la même connexion, c'est donc tout simplement naturel.

Étape 8: affiner notre classe

Nous pouvons faire plusieurs choses à partir d'ici. Pour vous assurer que les connexions sont fermées et renvoyées au pool, procédez comme suit dans DAOManager:

@Override
protected void finalize()
{

    try{ this.close(); }
    finally{ super.finalize(); }

}

Vous pouvez également implémenter des méthodes qui encapsulent setAutoCommit(), commit() et rollback() à partir de Connection afin que vous puissiez avoir une meilleure gestion de vos transactions. Ce que j'ai également fait, au lieu de simplement tenir un Connection, DAOManager contient également un PreparedStatement et un ResultSet. Ainsi, lors de l'appel de close(), il ferme également les deux. Un moyen rapide de clôturer des déclarations et des jeux de résultats!

J'espère que ce guide pourra vous être utile dans votre prochain projet!

84
Carlos Vergara

Je pense que si vous voulez faire un modèle DAO simple en JDBC ordinaire, vous devriez rester simple:

      public List<Customer> listCustomers() {
            List<Customer> list = new ArrayList<>();
            try (Connection conn = getConnection();
                 Statement s = conn.createStatement();
                 ResultSet rs = s.executeQuery("select * from customers")) { 
                while (rs.next()) {
                    list.add(processRow(rs));
                }
                return list;
            } catch (SQLException e) {
                throw new RuntimeException(e.getMessage(), e); //or your exceptions
            }
        }

Vous pouvez suivre ce modèle dans une classe appelée par exemple CustomersDao ou CustomerManager, et vous pouvez l'appeler avec un simple

CustomersDao dao = new CustomersDao();
List<Customers> customers = dao.listCustomers();

Notez que j'utilise try with resources et que ce code est sans danger pour les fuites de connexions, propre et simple, vous ne voulez probablement pas suivre le modèle DAO complet avec les usines, les interfaces et toute la plomberie qui, dans de nombreux cas, ne le font pas. ajouter de la valeur réelle.

Je ne pense pas que ce soit une bonne idée d'utiliser ThreadLocals, Bad utilisé comme dans la réponse acceptée est une source de fuites du chargeur de classe

Rappelez-vous TOUJOURS de fermer vos ressources (instructions, ensembles de résultats, connexions) dans un bloc try finally ou en utilisant try avec des ressources

7