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.
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 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.
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 |
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
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 ?!
E-mail: precisoestudarsempre@gmail.com
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:
Parabéns pela solução e pela postagem!
Imenso? kkkkkkkkkkkk
Postar um comentário