quinta-feira, 25 de fevereiro de 2016

De um DAO genérico a uma API de persistência

Bem-vindos ao blog Preciso Estudar Sempre. Meu nome é João Paulo Maida e minha paixão é estudar.

O tema que iremos discutir hoje, foi construído em parte em um post passado aqui do blog. Nele, conseguimos projetar uma classe DAO totalmente genérica e independente. O resultado foi: muitas sugestões e elogios. Isso é ótimo !! É disso que precisamos !!

Se você chegou aqui agora e não tem idéia do que eu to falando. Antes de tudo, seja bem-vindo. Agora, sugiro que você dê uma lidinha no post abaixo.

Um DAO totalmente genérico e independente

Hoje, discutiremos a evolução do post acima. Hoje, sairemos de um DAO genérico e iremos para uma API de persistência.

Achou ousado ??? Se não sonharmos alto, não atingimos novos níveis.

Análise da solução

Para iniciarmos nosso debate, é necessário que você tenha um conhecimento médio/avançado em Java, OO, UML e do padrão de projeto DAO. Nosso projeto será construído em Java 7, devido aos recursos que essa versão oferece e será dividido em dois módulos.

  • Módulo PersistenceAPI - Representa o projeto da API, de fato.
  • Módulo PersistenceAPIClient - Representa um projeto de software que, usa a nossa API para persistir e recuperar seus dados.
A figura 1 representa o diagrama de classes completo (PersistenceAPI + PersistenceAPIClient). Vamos analisar a estrutura de pacotes.
Figura 1 - Diagrama de classes completo (PersistenceAPI + PersistenceAPIClient)
  • client - Pacote de representação para as classes clientes, ou seja, as classes que acessam a nossa API.
    • dao - Pacote das classes DAO.
    • entidade - Pacote das classes de domínio.
    • exception - Pacote para exceptions.
  • br.com.persistenceapi.core - Pacote que encapsula todas as classes da API.
    • dao - Pacote da classe DAO genérica.
    • datasource - Pacote da classe que gerencia o datasource da API.
    • exception - Pacote para exceptions
    • pool - Pacote das classes que gerenciam o pool.
A classe DAO cliente deve estender (estabelecer uma relação de herança) a classe DAO genérica da API. A partir do momento que isso é feito, a subclasse tem acesso aos métodos públicos da superclasse a qual, tem uma relação de composição com a classe DataSource e um relacionamento de uso com a interface RowMapping, a qual é genérica.

A interface tem um único método chamado mapping. Este método é responsável pelo mapeamento do objeto ResultSet em uma classe de domínio. Através da composição, a superclasse tem acesso a métodos para obter e fechar conexões do pool.

O DataSource é composto pelo pool visto que, representa uma camada intermediária entre a classe GenericDAO e o pool. Sua responsabilidade é prover acesso a obtenção de conexões e encapsular a inteligência para o encerramento.

Por último mas, não menos importante, temos a classe JDBCConnectionPool. Ela é responsável por ler o arquivo de propriedades de configuração e validar seus dados, inicializar o pool com as configurações passadas, iniciar a thread de timeout (caso configurada) e encerrar todas as conexões. A thread de timeout tem um papel chave no funcionamento do pool. Ela é iniciada no construtor e controla o tempo de timeout definido no arquivo de configuração.

Iremos abordar com mais detalhes o funcionamento de cada classe em seções posteriores.

Banco de dados

Precisamos antes de tudo, construir nossa base de dados. Não irei por restrições sobre sua preferência de banco de dados. Eu estou utilizando o MySQL mas, se você quiser usar o Postgres, Oracle ou DB2, sem problemas. Lembre-se somente que você deve ter o JAR do drive desse banco porque senão, nem poderemos começar nosso projeto de API.
  CREATE TABLE `funcionario` (   
  `ID` int(11) NOT NULL AUTO_INCREMENT,   
  `NM_FUNCIONARIO` varchar(45) DEFAULT NULL,   
  `EM_FUNCIONARIO` varchar(45) DEFAULT NULL,   
  `DT_NASCIMENTO_FUNCIONARIO` date DEFAULT NULL,   
  `MAT_FUNCIONARIO` varchar(45) DEFAULT NULL,   
  `NM_LOGRADOURO` varchar(45) DEFAULT NULL,   
  `NUM_LOGRADOURO` int(11) DEFAULT NULL,   
  `NM_BAIRRO` varchar(45) DEFAULT NULL,   
  PRIMARY KEY (`ID`)   
  ) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8$$   

OBSERVAÇÃO: Se você já tem a tabela do post anterior, não precisa executar o script SQL pois, é a mesma tabela.

PersistenceAPI

A nossa API será um projeto Java comum, independente da IDE que você usar. Já sabemos quais pacotes precisamos criar, vamos analisar agora as classes de cada pacote e entender os conceitos enraizados em cada uma delas.

Antes de começarmos, é importante citar que as classes do pacote de exceção dispensam comentários porque suas implementações não fogem da normalidade. Elas foram projetadas para terem nomes auto explicativos facilitando assim, a legibilidade do código. Exceções são usadas como sinalizadoras de erros ou inconformidades.

Para que possamos entender como a API funciona, precisamos antes, entender como um pool funciona. O conceito de pool não está preso somente a conexões de banco de dados. É possível criar um pool com conexões a arquivos, por exemplo. Um pool é repositório de recursos os quais, são gerenciados visando maior performance.

Vamos facilitar !!

Na abordagem tradicional Java + DAO, geralmente uma conexão é aberta, no início de um bloco try, e fechada, dentro de um bloco finally, para todos os métodos de uma classe DAO. Em geral, isso é péssimo para o desempenho do sistema pois, causa lentidão. Abrir e fechar uma conexão é algo que pode demorar visto que, diversas tarefas precisam ser realizadas, como por exemplo: sockets (se você não sabe o que um socket, dá uma olhada nas referências) devem ser abertos e conectados, questões de concorrência, a porta pode estar sendo ocupada por outro processo, entre outras coisas. Então, qual é a solução para este grande problema visto que, precisamos abrir uma conexão ? Será que deixar uma conexão aberta por todo o tempo de vida de uma aplicação, é uma abordagem melhor ? A resposta para essas perguntas é: o pool de conexões.

A lógica de um pool de conexões é fácil. Deve ser criada uma lista de tamanho determinado e todas as posições são preenchidas com conexões abertas. A partir do momento que um método precisa executar operações no banco, ele solicita uma conexão. O pool analisa essa solicitação e verifica a disponibilidade de suas conexões. Caso alguma esteja disponível, ela é retornada para o método. Caso contrário, uma exceção pode ser propagada. Para o código cliente, a impressão passada é que realmente uma nova conexão está sendo aberta. Quando o método termina de executar suas operações, ele "fecha" a conexão. O processo de encerramento de conexões consiste em retornar a instância outrora usada, para a lista interna do pool. A codificação cliente não tem sequer idéia de todo o controle realizado por trás.

Vendo por esse aspecto, você deve estar pensando que criar um pool próprio é mole. Contudo, existem outros fatores ainda não debatidos.

Uma vez inicializado e configurado, o pool deve permanecer quanto tempo em memória ? Se você está pensando que ele deve ficar ativo enquanto a aplicação estiver funcionando, você sabe que isso não é viável para uma aplicação desktop GUI (Swing) ou web. Não seria correto deixar o pool ativo, cheio de conexões, não tendo nenhuma requisição de conexão ao banco. Contudo, como é possível identificar o momento em que devemos "desligar" o pool, sem desligar a aplicação ?

Para que essa pergunta seja respondida, é interessante que conheçamos o conceito de timeout. O timeout é o tempo que algum recurso permanece ativo a partir do momento que está ocioso. Logo, é possível concluir que nosso pool precisa de um timeout. Porém, a rotina do timeout deve ser iniciada logo após, o pool ter sido inicializado. Então, precisamos executar essas tarefas de forma paralela.

A estrutura computacional oferecida para as linguagens de programação para executar tarefa com tal característica, é uma thread. Se você não está familiarizado, dê uma clicada aqui e fique por dentro.

Depois que o timeout expira, as conexões do pool devem ser encerradas. Porém, como o pool deve se comportar, se logo após que o timeout expirou, ele pode receber uma nova solicitação de obtenção de conexão ? Lembre-se que o fato do pool ter expirado, não significa que o programa principal chegou ao seu fim. Para cada solicitação feita ao pool, este deve verificar se suas conexões ainda estão ativas. Caso não estejam, são reativadas.

E a questão da concorrência ? Como o pool deve se comportar caso, dois método sejam executados na mesma hora e tentem obter conexões ? Essa questão responderemos mais a frente quando analisarmos o código-fonte do nosso pool.

Acho que já abordamos bem a parte teórica, vamos para a parte prática !!!

Classe GenericDAO e interface RowMapping

 package br.com.persistenceapi.core.dao;  
   
 import br.com.persistenceapi.core.datasource.DataSource;  
 import br.com.persistenceapi.core.exception.EmptyPoolException;  
 import br.com.persistenceapi.core.exception.EmptyResultSetException;  
 import br.com.persistenceapi.core.exception.MoreThanOneResultException;  
 import java.sql.Connection;  
 import java.sql.PreparedStatement;  
 import java.sql.ResultSet;  
 import java.sql.SQLException;  
 import java.util.ArrayList;  
 import java.util.List;  
   
 /**  
  * Classe genérica que representa o DAO Genérico.  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  * @param <T> Notação genérica da classe  
  */  
 public class GenericDAO<T>{  
   
   private final DataSource dataSource;  
   
   /**  
    * Construtor da classe. Inicializa o data source.  
    */  
   public GenericDAO(){  
     dataSource = new DataSource();  
   }  
   
   /**  
    * Implementação de método que é responsável por realizar as operações de escrita (insert, update, delete) no banco de dados.  
    * @param sql Representa a string sql.  
    * @param parametros Representa a lista de parâmetros da query.  
    * @throws br.com.persistenceapi.core.exception.EmptyPoolException Representa o momento em que o pool não possui conexões disponíveis.  
    * @throws java.sql.SQLException Representa algum erro de SQL ou de conexão.  
    */  
   public void insertUpdateDelete(String sql, List<Object> parametros) throws EmptyPoolException, SQLException{  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     try {  
       connection = dataSource.getConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       preparedStatement.execute();  
       connection.commit();  
     } catch (SQLException ex) {  
       if(connection != null){  
         connection.rollback();  
       }  
       throw ex;  
     } catch (EmptyPoolException ex) {  
       throw ex;  
     } finally {  
       dataSource.closeConnection(connection, preparedStatement);  
     }  
   }  
     
   /**  
    * Implementação de método que é responsável por receber os parâmetros, avaliar se algum deles é nulo e configurá-los no statement.  
    * @param preparedStatement Representa o statement oriundo da query.  
    * @param parametros Representa a lista de parâmetros da query.  
    * @throws SQLException Representa algum erro da atribuição dos parâmetros da query ao statement.  
    */  
   private void receiveParameters(PreparedStatement preparedStatement, List<Object> parametros) throws SQLException{  
     int paramPos = 1;  
     for(Object parametro : parametros){  
       if(parametro == null){  
         preparedStatement.setNull(paramPos, 1);  
       } else {  
         preparedStatement.setObject(paramPos, parametro);  
       }  
       paramPos++;  
     }  
   }  
     
   /**  
    * Implementação de método responsável por realizar operações de leitura no banco de dados (select) as quais, podem retornar vários registros.  
    * @param sql Representa a query a ser executada.  
    * @param parametros Representa a lista de parâmetros da query.  
    * @param rowMapping Representa o mapeamento do resultado da query com os objetos de entidade.  
    * @return Retorna uma lista genérica de objetos oriundos da consulta SQL.  
    * @throws java.sql.SQLException Representa algum erro de SQL ou de conexão.  
    * @throws br.com.persistenceapi.core.exception.EmptyPoolException Representa o momento em que o pool não possui conexões disponíveis.  
    * @throws br.com.persistenceapi.core.exception.EmptyResultSetException Representa que a consulta realizada não retornou nenhum dado.  
    */  
   public List<T> findAll(String sql, List<Object> parametros, RowMapping rowMapping) throws SQLException, EmptyPoolException, EmptyResultSetException{  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     ResultSet resultSet = null;  
     List<T> rows = new ArrayList();  
     try {  
       connection = dataSource.getConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       resultSet = preparedStatement.executeQuery();  
       if(!resultSet.isBeforeFirst()){  
         throw new EmptyResultSetException("A consulta SQL realizada não possui resultados.");  
       }  
       while(resultSet.next()){  
         rows.add((T) rowMapping.mapping(resultSet));  
       }        
       connection.commit();  
     } catch (SQLException | EmptyPoolException ex) {  
       throw ex;  
     } finally {  
       dataSource.closeConnection(connection, preparedStatement, resultSet);  
     }  
     return rows;  
   }  
   
   /**  
    * Implementação de método responsável por realizar operações de leitura no banco de dados (select) as quais, somente retornam um registro.  
    * @param sql Representa a query a ser executada.  
    * @param parametros Representa a lista de parâmetros da query.  
    * @param rowMapping Representa o mapeamento do resultado da query com os objetos de entidade.  
    * @return Retorna o objeto genérico oriundo da consulta SQL.  
    * @throws br.com.persistenceapi.core.exception.EmptyResultSetException Representa que a consulta realizada não retornou nenhum dado.  
    * @throws br.com.persistenceapi.core.exception.MoreThanOneResultException Representa que a consulta realizada retornou mais de um registro.  
    * @throws br.com.persistenceapi.core.exception.EmptyPoolException Representa o momento em que o pool não possui conexões disponíveis.  
    * @throws java.sql.SQLException Representa algum erro de SQL ou de conexão.  
    */  
   public T findById(String sql, List<Object> parametros, RowMapping rowMapping) throws EmptyResultSetException, MoreThanOneResultException, EmptyPoolException, SQLException{  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     ResultSet resultSet = null;  
     T row = null;  
     try {  
       connection = dataSource.getConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       resultSet = preparedStatement.executeQuery();  
       if(!resultSet.isBeforeFirst()){  
         throw new EmptyResultSetException("A consulta SQL realizada não possui resultados.");  
       }  
       resultSet.last();  
       if(resultSet.getRow() > 1){  
         throw new MoreThanOneResultException("A consulta SQL retorna mais de um resultado.");  
       }  
       resultSet.first();  
       row = (T) rowMapping.mapping(resultSet);  
       connection.commit();  
     } catch (SQLException | EmptyPoolException ex) {  
       throw ex;  
     } finally {  
       dataSource.closeConnection(connection, preparedStatement, resultSet);  
     }  
     return row;  
   }  
 }  
   


 package br.com.persistenceapi.core.dao;  
   
 import java.sql.ResultSet;  
 import java.sql.SQLException;  
   
 /**  
  * Inteface genérica do mapeamento resultado do banco(result set) - objeto de domínio  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  * @param <T> Notação genérica da interface.  
  */  
 public interface RowMapping<T> {  
     
   /**  
    * Método que realiza o mapeamento do result set em um objeto de domínio.  
    * @param resultSet Objeto do tipo ResultSet. Contém o retorno da query.  
    * @return T Retorna um objeto especificado pela generic.  
    * @throws SQLException Caso algum erro aconteça quando for acessar os resultados da query.  
    */  
   T mapping(ResultSet resultSet) throws SQLException;  
 }  

Estas são a classe DAO genérica e a interface de método único, citados anteriormente. Em ambas, é possível identificar o forte uso de generics (recurso Java 5+) visto que, o objetivo principal é a reutilização máxima de código para as operações de banco. Logo, em vários pontos é possível notar que o tipo do retorno de métodos é inferido em tempo de execução. A propagação de exceções é feita com o fim de, sinalizar inconformidades durante a execução.

O método receiveParameters é responsável por receber os parâmetros de uma query e configurá-los no objeto PreparedStatement. Na sua implementação, foi usado o método setObject visto que, segundo a documentação, ele define em runtime qual o tipo do dado passado e atribui corretamente seu valor à query. Eliminando assim, uma extensa verificação.

Classe DataSource


 package br.com.persistenceapi.core.datasource;  
   
 import br.com.persistenceapi.core.exception.EmptyPoolException;  
 import br.com.persistenceapi.core.exception.PoolCreationException;  
 import br.com.persistenceapi.core.pool.JDBCConnectionPool;  
 import java.sql.Connection;  
 import java.sql.PreparedStatement;  
 import java.sql.ResultSet;  
 import java.sql.SQLException;  
   
 /**  
  * Classe criada com a finalidade de representar um data source. Esta classe constitui uma camada intermediária entre o pool de conexões e o dao genérico.  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  */  
 public class DataSource {  
   
   /*instância do pool*/  
   private JDBCConnectionPool pool;  
   
   /**  
    * Construtor da classe DataSource. Inicializa o pool de conexões.     
    */  
   public DataSource() {  
     try{  
       this.pool = new JDBCConnectionPool();  
     } catch (PoolCreationException pce){  
       pce.printStackTrace();  
     }  
   }  
   
   /**  
    * Obtém uma conexão disponível do pool.  
    * @return Representa a conexão.  
    * @throws SQLException Representa um erro de conexão a base de dados.  
    * @throws br.com.persistenceapi.core.exception.EmptyPoolException Representa o momento em que o pool não possui conexões disponíveis.  
    */  
   public Connection getConnection() throws SQLException, EmptyPoolException {  
     return pool.getConnection();  
   }  
   
   /**  
    * Retorna a conexão ao pool.  
    * @param connection Representa a conexão.  
    */  
   public void closeConnection(Connection connection) {  
     pool.returnConnection(connection);  
   }  
     
   /**  
    * Realiza o encerramento do statement atrelado à conexão e devolve a conexão ao pool.  
    * @param connection Representa a conexão.  
    * @param preparedStatement Representa o statement.  
    */  
   public void closeConnection(Connection connection, PreparedStatement preparedStatement) {  
     if(preparedStatement != null){  
       try {  
         preparedStatement.close();  
       } catch (SQLException ex) {  
         ex.printStackTrace();  
       }  
     }  
     this.closeConnection(connection);  
   }  
     
   /**  
    * Realiza o encerramento do result set, statement e devolve a conexão ao pool.  
    * @param connection Representa a conexão.  
    * @param preparedStatement Representa o statement.  
    * @param resultSet Representa o result set.  
    */  
   public void closeConnection(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet){  
     if(resultSet != null){  
       try {  
         resultSet.close();  
       } catch (SQLException ex) {  
         ex.printStackTrace();  
       }  
     }  
     this.closeConnection(connection, preparedStatement);  
   }  
 }  
   

Essa classe é responsável pela comunicação da classe GenericDAO com o pool. Ela atua como uma cada camada intermediária, inicializando o pool, encaminhando as solicitações de conexão e provendo uma abstração para o encerramento de conexões. Essa abstração omite o fato de que o encerramento da conexão, aqui,  não existe. O que realmente existe é o retorno da conexão para o pool e encerramento dos objetos PreparedStatement e ResultSet. O verdadeiro encerramento é feito em outro ponto, o qual já vimos.

Classes JDBCConnectionPool e TerminatePoolThread


 package br.com.persistenceapi.core.pool;  
   
 import br.com.persistenceapi.core.exception.EmptyPoolException;  
 import br.com.persistenceapi.core.exception.PoolCreationException;  
 import br.com.persistenceapi.core.exception.PropertiesConfigurationException;  
 import java.io.File;  
 import java.io.FileInputStream;  
 import java.io.IOException;  
 import java.sql.Connection;  
 import java.sql.DriverManager;  
 import java.sql.SQLException;  
 import java.util.ArrayList;  
 import java.util.List;  
 import java.util.Properties;  
   
 /**  
  * Classe que representa o pool de conexões.  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  */  
 public class JDBCConnectionPool {  
   
   /*pool*/  
   private static List<Connection> connectionPool;  
     
   /*dados do arquivo de propriedades*/  
   private static Integer poolSize;  
   private static String driver;  
   private static String user;  
   private static String pass;  
   private static String host;  
   private static String databaseName;  
   private static Integer timeout;  
   private static boolean isNeverTimeout;  
   
   /*flag para controle da leitura do arquivo de propriedades*/  
   private static boolean isPoolAlreadyConfigured;  
   
   private final String MSG_PROPERTY_CONFIGURATION_EXCEPTION = "Erro ao realizar a configuração do pool.";  
   
   /**  
    * Construtor da classe.  
    * @throws br.com.persistenceapi.core.exception.PoolCreationException Representa um erro de criação do pool.  
    */  
   public JDBCConnectionPool() throws PoolCreationException {    
     try {  
       if(!isPoolAlreadyConfigured){  
         this.initializeConfiguration();  
         this.connectionPool = new ArrayList<>(this.poolSize);  
         this.initializeConnectionPool();  
         if(!isNeverTimeout){  
           new TerminatePoolThread(this).start();  
         }  
       }  
     } catch (PropertiesConfigurationException | SQLException ex) {  
       throw new PoolCreationException("Erro na criação do pool.", ex);  
     }  
   }  
   
   /**  
    * Implementação de método responsável por ler todos os dados do arquivo de propriedades.  
    * @throws PropertiesConfigurationException Representa um erro na leitura do arquivo de propriedades. Este erro pode ser ocasionado pela ausência do arquivo, nome de arquivo incorreto ou algum erro de I/O no fechamento do arquivo.  
    */  
   private void initializeConfiguration() throws PropertiesConfigurationException {  
     Properties poolProperties = new Properties();  
     FileInputStream fis = null;  
     try {  
       fis = new FileInputStream(new File("..//database.properties"));  
       poolProperties.load(fis);  
       this.validateConfigurations(poolProperties);  
     } catch (IOException ioe) {  
       throw new PropertiesConfigurationException("Erro ao ler o arquivo de configuração. Arquivo inexistente ou o nome de arquivo de configuração incorreto. O nome deve ser 'database.properties'.", ioe);  
     } finally {  
       if (fis != null) {  
         try {  
           fis.close();  
         } catch (IOException ioe) {  
           throw new PropertiesConfigurationException("Erro ao fechar o arquivo de configuração.", ioe);  
         }  
       }  
     }  
   }  
   
   /**  
    * Realiza a validação dos dados oriundos do arquivo de propriedades.  
    * @param poolProperties Representa o arquivo de propriedades.  
    * @throws PropertiesConfigurationException Representa um erro na configuração, exemplos: ausência de dados obrigatórios,   
    * dados em formatos incorretos, valores abaixo ou acima do permitido.  
    */  
   private void validateConfigurations(Properties poolProperties) throws PropertiesConfigurationException{      
     this.isPoolAlreadyConfigured = true;  
   
     String poolSize = poolProperties.getProperty("poolSize");  
     if("".equals(poolSize)){  
       //valor default  
       this.poolSize = 10;  
     } else {  
       try {  
         this.poolSize = Integer.parseInt(poolSize);  
       } catch (NumberFormatException nfe) {  
         throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " O valor para o tamanho do pool deve ser inteiro.");  
       }  
   
       if(this.poolSize > 30 || this.poolSize < 10){  
         throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION +   
           " Tamanho do pool acima ou abaixo do permitido. O tamanho do pool deve estar entre 10 e 30, inclusive.");  
       }  
     }  
   
     this.user = poolProperties.getProperty("user");  
     if("".equals(this.user)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " Usuário do banco não especificado. Este campo é obrigatório.");  
     }  
   
     this.pass = poolProperties.getProperty("pass");  
     if("".equals(this.pass)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " Senha do banco não especificada. Este campo é obrigatório.");  
     }  
   
     this.driver = poolProperties.getProperty("driver");  
     if("".equals(this.driver)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " Driver do banco não especificado. Este campo é obrigatório.");  
     }  
   
     this.host = poolProperties.getProperty("host");  
     if("".equals(this.driver)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " Host do banco não especificado. Este campo é obrigatório.");  
     }  
       
     this.databaseName = poolProperties.getProperty("databaseName");  
     if("".equals(this.driver)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " Nome do banco não especificado. Este campo é obrigatório.");  
     }  
       
     String timeout = poolProperties.getProperty("timeout");  
     if("".equals(timeout)){  
       //valor default  
       this.timeout = 30;  
     } else {  
       try {  
         this.timeout = Integer.parseInt(timeout);  
       } catch (NumberFormatException nfe) {  
         throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " O valor para o timeout deve ser inteiro.");  
       }  
     }  
           
     String neverTimeout = poolProperties.getProperty("neverTimeout");  
     if(!"true".equalsIgnoreCase(neverTimeout) && !"false".equalsIgnoreCase(neverTimeout)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION + " O valor para a flag deve ser 'true' ou 'false'.");  
     }  
     this.isNeverTimeout = Boolean.valueOf(neverTimeout);  
   
     if(!this.isNeverTimeout && (this.timeout > 60 || this.timeout < 10)){  
       throw new PropertiesConfigurationException(MSG_PROPERTY_CONFIGURATION_EXCEPTION +   
         " O tempo de timeout acima ou abaixo do permitido. O intervalo de tempo deve estar entre 10 e 60 segundos, inclusive.");  
     }  
   }  
   
   /**  
    * Implementação de método que inicializa as conexões no pool.  
    * @throws SQLException Representa um erro da criação da conexão.  
    * @throws PropertiesConfigurationException Representa um erro na leitura dos dados do arquivo.  
    */  
   private void initializeConnectionPool() throws SQLException, PropertiesConfigurationException {  
     while (!checkIfConnectionPoolIsFull()) {  
       connectionPool.add(createNewConnection());  
     }  
   }  
   
   /**  
    * Implementação de método que verifica se pool de conexões está cheio ou não.  
    * @return Retorna true caso o pool esteja cheio. Caso contrário, retorna false.  
    */  
   protected boolean checkIfConnectionPoolIsFull() {  
     return connectionPool.size() == this.poolSize;  
   }  
   
   /**  
    * Implementação de método responsável por criar uma nova conexão com a base de dados.  
    * @return Retorna a conexão criada com a base de dados.  
    * @throws SQLException Representa um erro na criação de uma conexão.  
    * @throws PropertiesConfigurationException Representa um erro no driver de banco especificado.  
    */  
   private Connection createNewConnection() throws SQLException, PropertiesConfigurationException {  
     try {  
       Class.forName(this.driver);  
     } catch (ClassNotFoundException cnfe) {  
       throw new PropertiesConfigurationException("Erro na leitura do arquivo de configuração. Verifique o valor referente ao driver do banco de dados.");  
     }  
     return DriverManager.getConnection(this.host + this.databaseName, this.user, this.pass);  
   }  
   
   /**  
    * Implementação de método sincronizado para a obtenção de conexão.  
    * @return Representa a conexão retornada do pool.  
    * @throws EmptyPoolException Representa o momento em que o pool não possui conexões disponíveis.  
    */  
   public synchronized Connection getConnection() throws EmptyPoolException {  
     if (connectionPool.size() > 0) {  
       Connection connection = connectionPool.get(0);  
       connectionPool.remove(0);  
       return connection;  
     } else {  
       throw new EmptyPoolException("O pool não possui conexões disponíveis no momento.");  
     }      
   }  
   
   /**  
    * Implementação de método que retorna a conexão para o pool.  
    * @param connection Representa a conexão.  
    */  
   public synchronized void returnConnection(Connection connection) {  
     connectionPool.add(connection);  
   }  
   
   /**  
    * Método sobreescrito de Object. Este método encerra todas as conexões caso o objeto do pool seja destruído.  
    * @throws Throwable Representa qualquer exceção.  
    */  
   @Override  
   protected void finalize() throws Throwable {  
     try {  
       this.terminateAllConnections();  
     } finally {  
       super.finalize();  
     }  
   }  
   
   /**  
    * Implementação de método que encerra todas as conexões.  
    */  
   protected void terminateAllConnections() {  
     for (Connection connection : connectionPool) {  
       try {  
         connection.close();  
       } catch (SQLException ex) {  
         ex.printStackTrace();  
       }  
     }  
     connectionPool.clear();  
     isPoolAlreadyConfigured = false;  
   }  
   
   /**  
    * @return the timeout  
    */  
   public Integer getTimeout() {  
     return timeout;  
   }  
 }  
   

A classe acima representa a implementação do pool. Acredito que se debatêssemos todos os métodos, a leitura desse texto ficaria extremamente extenso. Então, comentarei sobre os pontos cruciais para o funcionamento.

No construtor da classe é onde, o pool é configurado e inicializado. Sua configuração é feita através de um arquivo de propriedades, representado pela figura 2. Todas as entradas do arquivo são lidas, validadas e então armazenados na classe. Após isso, o pool é inicializado, ou seja, as conexões são criadas. A quantidade de conexões criadas é definida pelo atributo poolSize do arquivo de configuração.

Figura 2 - O arquivo de propriedades de configuração
Outro atributo do arquivo de configuração que, desempenha papel importante na configuração, é o neverTimeout. Este só pode receber dois valores: true e false. Caso seu valor seja falso, a lógica de timeout deve ser aplicada e então, a thread de timeout é iniciada. Caso contrário, o pool não possui timeout.

Os atributos da classe referente a configuração do pool, a lista de conexões, e o booleano isPoolAlreadyConfigured precisam ser estáticos porque, só é necessário que o pool seja configurado e iniciado uma única vez, na sua criação. O responsável por realizar todo esse controle é o último atributo citado. Caso as conexões do pool sejam destruídas pela rotina de timeout, o flag de controle de configuração volta a ser falso e o pool é reconfigurado e reinicializado.

É necessário que tanto o método getConnection quanto o returnConnection sejam synchronized para que não haja problema de concorrência a recursos, no acesso simultâneo de várias classes clientes.

 package br.com.persistenceapi.core.pool;  
   
 /**  
  * Thread usada para a liberação das conexões do pool. É necessário utilizar uma thread para a realizar a liberação pois,   
  * essa tarefa deve ser feito de forma paralela juntamente com as tarefas desempenhadas pelo pool.  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  */  
 public class TerminatePoolThread extends Thread {  
   
   /*instância do pool*/  
   private final JDBCConnectionPool jdbcConnectionPool;  
   
   /**  
    * Construtor da Thread.  
    * @param jdbcConnectionPool Representa o pool de conexões. É necessário receber a instância do pool para que, possa  
    * realizar acesso a propriedade timeout.  
    */  
   public TerminatePoolThread(JDBCConnectionPool jdbcConnectionPool) {  
     this.jdbcConnectionPool = jdbcConnectionPool;  
   }  
     
   /**  
    * Implementação de método que contém o comportamento executado pela thread. A thread verificará com o delay de meio segundo  
    * se todas as conexões não estão sendo utilizadas. Se não estiverem, o contador do timeout começa sua contagem.  
    * Caso contrário, o contador é zerado e o código cliente pode usar a conexão.  
    */  
   @Override  
   public void run(){  
     super.run();  
     System.out.println("Thread de timeout iniciada");  
     int timeoutCounter = 0;  
     while (true) {  
       if (jdbcConnectionPool.checkIfConnectionPoolIsFull()) {  
         timeoutCounter++;  
         if (jdbcConnectionPool.getTimeout() == timeoutCounter) {  
           jdbcConnectionPool.terminateAllConnections();  
           break;  
         }  
       } else {  
         timeoutCounter = 0;  
       }  
       try {  
         Thread.sleep(500);  
       } catch (InterruptedException ex) {          
         ex.printStackTrace();  
       }  
     }  
     System.out.println("Thread de timeout terminada");  
   }  
 }  
   

A thread de timeout necessita de uma representação interna do pool pois, precisa acessar seus dados. A contagem só pode ser iniciada quando nenhuma conexão estiver sendo utilizada. O sleep é necessária pois, loops infinitos consomem muita CPU, podendo causar lentidão ou até, paralisação do programa.

Caso a contagem chegue ao fim, ou seja, o contador de timeout é igual ao valor definido no arquivo de configuração, o comando para encerramento real das conexões é dado. As conexões são encerradas, a lista é limpa e o flag de pool já configurado, citado anteriormente, volta ao valor default.

PersistenceAPIClient

Nosso projeto cliente também é um projeto Java comum e muito simples. Para que ele possa usar a nossa API recém criada, é necessário que tenha uma classe DAO que estenda a classe GenericDAO, da API e, que o driver do banco esteja em seu classpath.

 package dao;  
   
 import br.com.persistenceapi.core.dao.RowMapping;  
 import br.com.persistenceapi.core.dao.GenericDAO;  
 import br.com.persistenceapi.core.exception.EmptyPoolException;  
 import br.com.persistenceapi.core.exception.EmptyResultSetException;  
 import br.com.persistenceapi.core.exception.MoreThanOneResultException;  
 import entidade.Funcionario;  
 import exception.BusinessException;  
 import exception.IntegrationException;  
 import java.sql.ResultSet;  
 import java.sql.SQLException;  
 import java.util.ArrayList;  
 import java.util.List;  
   
 /**  
  * Classe cliente DAO da entidade Funcionario.  
  * @author Preciso Estudar Sempre - precisoestudarsempre@gmail.com  
  */  
 public class FuncionarioDAO extends GenericDAO<Funcionario>{  
   
   public void insert(Funcionario funcionario) throws IntegrationException, BusinessException{  
     String sql = "INSERT INTO funcionario ("  
         + "NM_FUNCIONARIO, "  
         + "EM_FUNCIONARIO,"  
         + "DT_NASCIMENTO_FUNCIONARIO,"  
         + "MAT_FUNCIONARIO,"  
         + "NM_LOGRADOURO,"  
         + "NUM_LOGRADOURO,"  
         + "NM_BAIRRO"  
         + ") VALUES (?,?,?,?,?,?,?)";  
     List<Object> parametros = new ArrayList<>();  
     parametros.add(funcionario.getNome());  
     parametros.add(funcionario.getEmail());  
     parametros.add(funcionario.getDataNascimento());  
     parametros.add(funcionario.getMatricula());  
     parametros.add(funcionario.getLogradouro());  
     parametros.add(funcionario.getNumero());  
     parametros.add(funcionario.getBairro());  
     try {  
       super.insertUpdateDelete(sql, parametros);  
     } catch (EmptyPoolException ex) {  
       throw new IntegrationException("Erro de integração com a base de dados.", ex);  
     } catch (SQLException ex) {  
       throw new BusinessException("Verifique a operação SQL.", ex);  
     }  
   }  
     
   public void update(Funcionario funcionario) throws IntegrationException, BusinessException{  
     String sql = "UPDATE FUNCIONARIO SET "  
         + "NM_FUNCIONARIO = ?, "  
         + "EM_FUNCIONARIO = ?,"  
         + "DT_NASCIMENTO_FUNCIONARIO = ?,"  
         + "MAT_FUNCIONARIO = ?,"  
         + "NM_LOGRADOURO = ?,"  
         + "NUM_LOGRADOURO = ?,"  
         + "NM_BAIRRO = ? "  
       + "WHERE ID = ?";  
     List<Object> parametros = new ArrayList<>();  
     parametros.add(funcionario.getNome());  
     parametros.add(funcionario.getEmail());  
     parametros.add(funcionario.getDataNascimento());  
     parametros.add(funcionario.getMatricula());  
     parametros.add(funcionario.getLogradouro());  
     parametros.add(funcionario.getNumero());  
     parametros.add(funcionario.getBairro());  
     parametros.add(funcionario.getId());  
     try {  
       super.insertUpdateDelete(sql, parametros);  
     } catch (EmptyPoolException ex) {  
       throw new IntegrationException("Erro de integração com a base de dados.", ex);  
     } catch (SQLException ex) {  
       throw new BusinessException("Verifique a operação SQL.", ex);  
     }  
   }  
     
   public void delete(Long id) throws IntegrationException, BusinessException{  
     String sql = "DELETE FROM FUNCIONARIO WHERE ID = ?";  
     List<Object> parametros = new ArrayList<>();  
     parametros.add(id);  
     try {  
       super.insertUpdateDelete(sql, parametros);  
     } catch (EmptyPoolException ex) {  
       throw new IntegrationException("Erro de integração com a base de dados.", ex);   
     } catch (SQLException ex) {  
       throw new BusinessException("Verifique a operação SQL.", ex);  
     }  
   }  
     
   public List<Funcionario> findAll() throws IntegrationException, BusinessException{  
     String sql = "SELECT * FROM FUNCIONARIO";  
     List<Object> parametros = new ArrayList<>();  
     try{  
       return super.findAll(sql, parametros, new RowMapping<Funcionario>() {  
         @Override  
         public Funcionario mapping(ResultSet resultSet) throws SQLException{  
           Funcionario funcionario = new Funcionario();  
           if(resultSet != null){  
             funcionario.setId(resultSet.getLong("ID"));  
             funcionario.setNome(resultSet.getString("NM_FUNCIONARIO"));  
             funcionario.setEmail(resultSet.getString("EM_FUNCIONARIO"));  
             funcionario.setDataNascimento(resultSet.getDate("DT_NASCIMENTO_FUNCIONARIO"));  
             funcionario.setMatricula(resultSet.getString("MAT_FUNCIONARIO"));  
             funcionario.setLogradouro(resultSet.getString("NM_LOGRADOURO"));  
             funcionario.setNumero(resultSet.getInt("NUM_LOGRADOURO"));  
             funcionario.setBairro(resultSet.getString("NM_BAIRRO"));  
           }  
           return funcionario;  
         }  
       });  
     } catch (EmptyPoolException | SQLException ex) {  
       throw new IntegrationException("Erro de integração com a base de dados.", ex);  
     } catch (EmptyResultSetException ex) {  
       throw new BusinessException(ex);  
     }  
   }  
     
   public Funcionario findById(Long id) throws IntegrationException, BusinessException{  
     String sql = "SELECT * FROM FUNCIONARIO WHERE ID = ?";  
     List<Object> parametros = new ArrayList<>();  
     parametros.add(id);  
     try {  
       return super.findById(sql, parametros, new RowMapping<Funcionario>() {  
         @Override  
         public Funcionario mapping(ResultSet resultSet) throws SQLException{  
           Funcionario funcionario = new Funcionario();  
           funcionario.setId(resultSet.getLong("ID"));  
           funcionario.setNome(resultSet.getString("NM_FUNCIONARIO"));  
           funcionario.setEmail(resultSet.getString("EM_FUNCIONARIO"));  
           funcionario.setDataNascimento(resultSet.getDate("DT_NASCIMENTO_FUNCIONARIO"));  
           funcionario.setMatricula(resultSet.getString("MAT_FUNCIONARIO"));  
           funcionario.setLogradouro(resultSet.getString("NM_LOGRADOURO"));  
           funcionario.setNumero(resultSet.getInt("NUM_LOGRADOURO"));  
           funcionario.setBairro(resultSet.getString("NM_BAIRRO"));  
           return funcionario;  
         }  
       });  
     } catch (EmptyResultSetException | MoreThanOneResultException ex) {  
       throw new BusinessException("A consulta realizada possui uma não conformidade de negócio.", ex);  
     } catch (EmptyPoolException | SQLException ex) {  
       throw new IntegrationException("Erro de integração com a base de dados.", ex);  
     }  
   }  
 }  
   

É possível notar que desenvolver uma camada de persistência usando nossa API, se tornou algo extremamente fácil e flexível. As únicas preocupações que sobraram para o desenvolvedor são:
  • Construir a query.
  • Por os parâmetros da query em uma lista.
  • Chamar o método da API.
  • Caso a query tenha retorno, implementar o método mapping.
  • Tratar possíveis exceções
As exceptions BusinessException IntegrationException foram criadas para representar, respectivamente, um erro de negócio e um erro de integração.  a finalidade de sinalizar alguma inconformidade. Elas não são necessárias em seu projeto, você trata as exceções que vem da API, da forma que você quiser.

Pronto !!!! Conseguimos acabar !! Uhullllll !! Até que enfim !!!

Acho que esse é o maior post do blog até o momento e foi mais de um mês de trabalho nele. Mas enfim, para trazer um conteúdo de qualidade para vocês, vale a pena. ;)

Agradecimentos:
Ao imenso amigo Luís Marcelo Bruckner. Sem você este projeto não seria possível - http://github.com/marcelobruckner
Ao colega Itacir Pompeu, do grupo do facebook DevCast - http://github.com/Pompeu
Ao colega Carlos Alexandre Becker, do grupo do facebook DevCast - http://github.com/caarlos0

Link no dropbox: https://www.dropbox.com/s/95w82mmhjp9gv58/PersistenceAPI.rar?dl=0
Link no github: https://github.com/PrecisoEstudarSempre/PersistenceAPI

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários, me mande um e-mail ou, na nossa página do facebook.

E-mail: precisoestudarsempre@gmail.com
Facebook: https://www.facebook.com/precisoestudarsempre/

Referências:
Interface PreparedStatement - https://docs.oracle.com/javase/7/docs/api/java/sql/PreparedStatement.html#setObject(int,%20java.lang.Object)
Need Code to create Connection Pool in java - http://stackoverflow.com/questions/2826212/need-code-to-create-connection-pool-in-java
Thread (ciência da computação) - https://pt.wikipedia.org/wiki/Thread_(ci%C3%AAncia_da_computa%C3%A7%C3%A3o)
Vamos fazer um chat ? - http://precisoestudarsempre.blogspot.com.br/2015/06/vamos-fazer-um-chat.html

Um comentário:

Luis Marcelo Bruckner disse...

Parabéns pela solução e pela postagem!
Imenso? kkkkkkkkkkkk