segunda-feira, 7 de dezembro de 2015

Um DAO totalmente genérico e independente

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

Hoje iremos ousar, hoje iremos ostentar no camarote. Se você lembrou da imagem abaixo, sim, você está certo.

Figura 1 - O rei do camarote
Seu sobrenome será "ostentação" mas, estamos aqui falando da ostentação do código-fonte. Eu afirmo com toda certeza que somente com Java e uma IDE, conseguimos criar uma camada DAO com um alto índice de código reutilizável. Contudo, a pergunta é: como ?

Se você é novo por aqui, bote seu email na caixa de texto para se inscrever no blog (clique aqui para ver como) ou curta a página do facebook, o link está no fim do post.

Voltando.....

Primeiro, precisamos entender como funciona um método que está na camada DAO. Se você não sabe o que é DAO, clique aqui ou dê uma olhada nas referências.
Figura 2 - O funcionamento de um método da camada DAO
É possível notar que algumas etapas sempre se repetem para todos os métodos DAO. Essas etapas são:
  • Abertura de conexão.
  • Configuração do commit automático.
  • Criação do statement.
  • Passagem de parâmetros para statement.
  • Execução do statement.
  • Commit da transação.
  • Rollback da transação (em caso de erro).
  • Encerramento da conexão.

E o que não se repete ? O que é diferente de um método para o outro ?
  • Query SQL
  • Parâmetros
Se pararmos para refletir sobre isso, realmente o que muda de um método para o outro é a query SQL realizada e os parâmetros que são passados. Então, temos muito código repetido o qual, podemos eliminar. Caso contrário, teremos problemas muito maiores no futuro. Mas, como eliminar ?

A resposta é simples. Temos que ter um bom projeto orientado a objetos onde, nossa meta é a reusabilidade de código.

----------------------------------------------------------------------------------------------------------

AVISO: É necessário o conhecimento dos seguintes assuntos para que, tenhamos um total aproveitamento do assunto:
  • Java
  • Orientação a objetos
  • Java + JDBC
  • SQL
  • UML
----------------------------------------------------------------------------------------------------------

Vamos modelar nossa solução.

Figura 3 - Diagrama de classes
GenericDAO

Essa superclasse possui os métodos que realizam as operações de banco (insert, update, delete, select) de forma genérica, configura os parâmetros da query, abrem conexão e encerram conexão. Como existem muitos métodos nessa classe, farei comentários pontuais sobre alguns.
  • insertUpdateDelete - Método responsável pelas operações de escrita (insert, update e delete). Consegue desempenhar a função genérica porque recebe como parâmetros, a query e os parâmetros da query e porque utiliza o método receiveParameters o qual, realiza a configuração dos parâmetros no objeto Statement. O índice dos parâmetros é conservado pela sua posição na lista.
  • receiveParameters - Método responsável pela configuração dos parâmetros da query recebidos no objeto Statement. Essa configuração é feita através de uma estrutura condicional que verifica o tipo da instância que está na lista de parâmetros. Uma vez que é determinado o tipo, o método correto do objeto Statement pode ser invocado. O índice dos parâmetros é conservado pela sua posição na lista.
  • findAll - Método responsável por listar registros de acordo com a query passada. O mapeamento dos dados do objeto ResultSet em objetos do tipo Funcionario acontece dentro da iteração do primeiro objeto e é feito através da implementação do método mapping.
  • findById - Método responsável por listar somente 1 registro de acordo com a query passada. Assim como no método acima, o mapeamento também é feito através do método mapping. Contudo, é necessário retornar sempre o primeiro registro portanto, uma verificação de cursor é feita em cima do retorno da query. Caso a query não tenha retornado nada, o objeto ResultSet se torna nulo e o método retorna um objeto zerado. Caso contrário, o cursor é movido em 1 posição e os dados são obtidos.
 public void insertUpdateDelete(String sql, List<Object> parametros){  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     try {  
       connection = this.openConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       preparedStatement.execute();  
       connection.commit();  
     } catch (SQLException ex) {  
       ex.printStackTrace();  
       if(connection != null){  
         try {  
           connection.rollback();  
         } catch (SQLException sqle) {  
           sqle.printStackTrace();  
         }  
       }  
     } finally {  
       this.closeConnection(connection, preparedStatement);  
     }  
   }  

   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 if(parametro instanceof Integer){  
         preparedStatement.setInt(paramPos, (Integer) parametro);  
       } else if(parametro instanceof Long){  
         preparedStatement.setLong(paramPos, (Long) parametro);  
       } else if(parametro instanceof Float){  
         preparedStatement.setFloat(paramPos, (Float) parametro);  
       } else if(parametro instanceof Double){  
         preparedStatement.setDouble(paramPos, (Double) parametro);  
       } else if(parametro instanceof Date){  
         preparedStatement.setDate(paramPos, new java.sql.Date(((Date) parametro).getTime()));  
       } else if(parametro instanceof String){  
         preparedStatement.setString(paramPos, parametro.toString());  
       }  
       paramPos++;  
     }  
   }  

   public List findAll(String sql, List<Object> parametros, RowMapping mapping){  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     ResultSet resultSet = null;  
     List rows = new ArrayList();  
     try {  
       connection = this.openConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       resultSet = preparedStatement.executeQuery();  
       while(resultSet.next()){  
         rows.add(mapping.mapping(resultSet));  
       }        
       connection.commit();  
     } catch (SQLException ex) {  
       ex.printStackTrace();  
       if(connection != null){  
         try {  
           connection.rollback();  
         } catch (SQLException sqle) {  
           sqle.printStackTrace();  
         }  
       }  
     } finally {  
       this.closeConnection(connection, preparedStatement, resultSet);  
     }  
     return rows;  
   }  

   public Object findById(String sql, List<Object> parametros, RowMapping mapping){  
     Connection connection = null;  
     PreparedStatement preparedStatement = null;  
     ResultSet resultSet = null;  
     Object row = null;  
     try {  
       connection = this.openConnection();  
       connection.setAutoCommit(false);  
       preparedStatement = connection.prepareStatement(sql);  
       this.receiveParameters(preparedStatement, parametros);  
       resultSet = preparedStatement.executeQuery();  
       resultSet = resultSet.isBeforeFirst() ? resultSet : null;  
       if(resultSet != null){  
         resultSet.next();  
       }  
       row = mapping.mapping(resultSet);  
       connection.commit();  
     } catch (SQLException ex) {  
       ex.printStackTrace();  
       if(connection != null){  
         try {  
           connection.rollback();  
         } catch (SQLException sqle) {  
           sqle.printStackTrace();  
         }  
       }  
     } finally {  
       this.closeConnection(connection, preparedStatement, resultSet);  
     }  
     return row;  
   }  
 }  

FuncionarioDAO

Esta é uma subclasse de GenericDAO. Utiliza os métodos da classe pai para persistir ou recuperar dados do banco de dados relacional. Os métodos de escrita são bem simples, englobam somente a criação da query e a adição dos parâmetros na lista. Os métodos que recuperam dados utilizam um recurso muito prático do Java, a implementação de interfaces em métodos. Dessa forma, eu não preciso criar uma classe nova a qual, implementa RowMapping. Eu posso passar diretamente a implementação da interface como um parâmetro do método. O método definido na interface RowMapping é o responsável pelo mapeamento ResultSet - Funcionario.

   public void insert(Funcionario funcionario){  
     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());  
     super.insertUpdateDelete(sql, parametros);  
   }  
   
   public List<Funcionario> findAll(){  
     String sql = "SELECT * FROM FUNCIONARIO";  
     List<Object> parametros = new ArrayList<>();  
     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;  
       }  
     });  
   }  
     
   public Funcionario findById(Long id){  
     String sql = "SELECT * FROM FUNCIONARIO WHERE ID = ?";  
     List<Object> parametros = new ArrayList<>();  
     parametros.add(id);  
     return (Funcionario) super.findById(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;  
       }  
     });  
   }  
 }  

RowMapping

Interface que possui único método, mapping. Este método é responsável pelo mapeamento do objeto ResultSet em uma entidade do sistema. Pelo fato de sempre retornar uma nova instância sendo essa, qualquer objeto do sistema, a interface é marcada como genérica . Portanto, o tipo de retorno também é genérico, ou seja, T.

Como citado anteriormente, a implementação dessa interface acontece na passagem de parâmetros dos métodos os quais, esperam por uma implementação da mesma.

 public interface RowMapping<T> {  
     
   /**  
    * Método que realiza o mapeamento do result set em um objeto.  
    * @param resultSet Objeto do tipo ResultSet. Contém o retorno da query.  
    * @return T Retorna um objeto especificado pela generic.  
    * @throws SQLException Erro de acesso ao banco ou SQL.  
    */  
   T mapping(ResultSet resultSet) throws SQLException;  
 }  

Funcionario

Um POJO comum.

Para criar a tabela do banco de dados, utilize o script abaixo:

 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$$  

Terminamos nosso DAO reutilizável e genérico. Sinta-se a vontade para criticar, comentar ou propor melhorias. Até porque é assim que conseguimos evoluir.

Baixe o projeto
Dropbox: https://www.dropbox.com/s/epg48k36xfp1brv/GenericDAO.rar?dl=0
GitHub: https://github.com/PrecisoEstudarSempre/GenericDAO.git

Agradecimentos a Bruno Souza.

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

Deixe aí nos comentários, na nossa página do facebook ou mande um email.

E-mail: precisoestudarsempre@gmail.com

Referências:
Objeto de acesso a dados - https://pt.wikipedia.org/wiki/DAO

3 comentários:

Luis Marcelo Bruckner disse...

Fala João, tudo bem?
Mais um ótimo post, parabéns!
Como conversamos, seria bacana adicionar o recurso de Generics nos métodos da classe GenericDao para evitar a possibilidade de erro de execução, tipo ClassCastException, onde a verificação do tipo ficaria em tempo de compilação.

Outro ponto que me incomodou um pouco é o fato da interface RowMapping não estar atrelada a classe GenericDao. Se deixar por conta do desenvolvedor implementar ou não a interface, perderia facilmente o controle do uso desse poderoso recurso.

Mas claro que tudo isso é no campo das ideias. Na prática a teoria é outra!

Um abraço meu amigo!

Preciso estudar sempre disse...

Tudo bem meu amigo. Melhor agora porque estou respondendo ao seu comentário.

Obrigado pela parabenização e contribuição. Tento sempre entregar o melhor conteúdo para vocês.

Anotei e gostei todas as suas contribuições. Breve, todas estarão em prática em um novo post.

Unknown disse...

Ótimo para o pessoal que está aprendendo JDBC