Atualmente o baixo acoplamento entre classes de um projeto é uma das principais características para definir um bom projeto, que pode ser alterado facilmente sem grandes impactos em toda sua estrutura.

Neste artigo estudaremos sobre o padrão para persistência de dados conhecido como DAO (um acrônimo para Data Access Object). Este tem por objetivo separar as regras de negócios das operações referentes ao banco de dados, além de evitar que outras partes do projeto precisem preocupar-se com persistência de dados quando este não é seu principal objetivo.

Trabalhando com o padrão DAO podemos mudar de banco de dados ou mesmo framework ORM sem que a aplicação tenha um grande impacto com isto.

Não vamos utilizar nenhum framework específico neste artigo, pois nosso foco será mostrar a construção de um DAO e sua utilização.

Iniciando a construção do DAO

Usaremos como banco de dados o PostgreSQL, mas este não é o ponto mais importante, até porque você poderá alterá-lo no DAO sem muitos problemas.

A classe da Listagem 1 contará com alguns métodos genéricos capazes de realizar operações de save, update, delete e find.

Listagem 1. A classe MainDAO


  import java.sql.Connection;
  import java.sql.DriverManager;
  import java.sql.PreparedStatement;
  import java.sql.ResultSet;
  import java.sql.SQLException;
   
   
  public class MainDAO {
         
         private static MainDAO instance;
         private Connection connection;
         
         private final String url = "jdbc:postgresql://localhost:5432/teste";
         private final String user = "postgres";
         private final String password = "pgadmin";
         
         public static MainDAO getInstance(){
               if (instance == null){
                      instance = new MainDAO();
               }            
               
               return instance;
         }
         
         private MainDAO(){
               try {
                      Class.forName("org.postgresql.Driver");
                      connection = DriverManager.getConnection(url, user, password);
               } catch (SQLException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
               } catch (ClassNotFoundException ex) {
                  Logger.getLogger(MainDAO.class.getName()).log(Level.SEVERE, null, ex);
              }
         }
         
         public Object execute(String sql, Object[] args){
               return execute(sql, args, false);
         }
         
         public ResultSet executeQuery(String sql, Object[] args){
               return (ResultSet) execute(sql, args, true);
         }
         
         private Object execute(String sql, Object[] args, boolean isQuery){
               try {
                      PreparedStatement pstm = connection.prepareStatement(sql);
                      
                      for (int i =0; i< args.length; i++){
                             pstm.setObject(i+1, args[i]);
                      }
                      
                      Object toReturn = null;
                      if (isQuery){
                             toReturn = pstm.executeQuery();
                      }else{
                             toReturn = pstm.execute();
                      }
                                          
                      return toReturn;
                      
               } catch (SQLException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                      return null;
               }
         }
   
  }

Vamos começar pelo padrão de projeto Singleton, que foi implementado no MainDAO. Este padrão é útil quando desejamos que uma classe possua apenas uma instância criada durante toda a aplicação.

O padrão de projeto singleton em nossa classe acima é composto pelo atributo:

private static MainDAO instance;

Juntamente com o método:

public static MainDAO getInstance(){
    if (instance == null){
         instance = new MainDAO();
    }          
               
    return instance;
  }

E o construtor da classe private MainDAO(){}, que tem seu acesso privado para garantir que em nenhum momento a classe poderá ser usada juntamente com a palavra reservada “new”. O único método pelo qual a classe MainDAO poderá ser chamada diretamente é o getInstance() e a partir deste todos os outros poderão ser chamados.

O atributo “instance” garante o armazenamento da única instância da classe durante a aplicação, sendo assim toda vez que chamarmos o método getInstance() é feita uma checagem se o atributo instance está nulo, caso verdadeiro ele irá chamar o construtor internamente, porém da próxima vez apenas o retorno de instance será realizado, sem a sua criação.

O construtor da classe é quem fica responsável por criar a conexão com o banco de dados.

A primeira etapa é carregar o driver “org.postgresql.Driver” e, para isso, usamos o método Class.forName(). Posteriormente criamos uma conexão usando o método getConnection() da classe DriverManager, onde o seu retorno será um objeto do tipo Connection, nossos parâmetros de criação da conexão estão declarados logo no início. Dessa forma, garantimos que eles não serão alterados em nenhum ponto, servem apenas para centralizar a informação.

O atributo “connection”, diferente do “instance”, não é e nem deve ser estático, pois ele só poderá ser acessado após retornada a instância de MainDAO. O bloco try-catch é obrigatório dada as exceções que são retornadas pelo forName() e pelo getConnection() do DriverManager.

No final deste artigo, na seção de downloads, você encontrará o driver JDBC para o PostgreSQL versão 9.1, mas você poderá também baixar direto do site do PostgreSQL caso utilize uma outra versão.

Agora já temos tudo preparado para começar a desenvolver os métodos que farão as operações de DML (Data Manipulation Language) com o banco de dados. Começaremos a explicar o método mais interno:


  private Object execute(String sql, Object[] args, boolean isQuery){
               try {
                      PreparedStatement pstm = connection.prepareStatement(sql);
                      
                      for (int i =0; i< args.length; i++){
                             pstm.setObject(i+1, args[i]);
                      }
                      
                      Object toReturn = null;
                      if (isQuery){
                             toReturn = pstm.executeQuery();
                      }else{
                             toReturn = pstm.execute();
                      }
                                          
                      return toReturn;
                      
               } catch (SQLException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                      return null;
               }
  }

O nosso método recebe três parâmetros:

  1. 'String sql', responsável por armazenar o comando SQL que será executado no banco de dados, lembrando que por tratar-se de um PreparedStatement os valores devem conter o sinal '?' de interrogação. Ex: INSERT INTO aluno (id, nome) values (?,?).
  2. 'Object[] args', que são os argumentos anexados ao SQL passado, substituindo o '?” pelo valor presente em args.
  3. 'boolean isQuery', que é usado para identificar se deverá ser retornado algo, pois trata-se de uma query, ou apenas executar um comando como INSERT, UPDATE ou DELETE sem nenhum retorno.

O próximo passo é retornar o preparedstatement para executar os comandos necessários:

PreparedStatement pstm = connection.prepareStatement(sql);

O nosso atributo connection criado através do construtor MainDAO() será o responsável por criar o PreparedStatement com o SQL que foi passado. Com este criado podemos fazer o 'binding' dos parâmetros nos pontos onde tem '?':

for (int i =0; i< args.length; i++){
         pstm.setObject(i+1, args[i]);
  }

A ideia é percorrer todos só elementos dentro do vetor args, atribuindo cada um desses no PreparedStatement através do índice atual. É muito importante que a ordenação dos argumentos seja o mesmo dos pontos de interrogação '?”, caso contrário a query será executada de forma errada ou uma exceção será retornada.

Feito o binding dos parâmetros nos seus devidos locais, estamos prontos para executar o comando SQL. Para isso, checamos se deve ser executada uma query com retorno ou não, usando o “isQuery”.

Caso seja uma query, então chamamos o pstm.executeQuery() que retorna um ResultSet, que possui o resultado da query (de um SELECT por exemplo); caso contrário, executamos o pstm.execute() que retorna apenas um booleano que indicará sucesso ou falha.

Lembrando que o método que explicamos anteriormente tem um acesso private, sendo assim, ele não pode ser chamado de fora da classe. Criamos outros dois métodos que servirão de fachada para acesso a este método mais restrito:


  public Object execute(String sql, Object[] args){
    return execute(sql, args, false);
  }

O primeiro é o execute(), que realiza a execução de um INSERT, UPDATE ou DELETE. Por isso passamos o atributo “isQuery” como false por padrão.

public ResultSet executeQuery(String sql, Object[] args){
         return (ResultSet) execute(sql, args, true);
  }

O outro método é o executeQuery(), onde, diferentemente do explicado anteriormente, este usa o boolean isQuery = true, e retorna um ResultSet, já fazendo um cast explícito para tal.

Com estes métodos implementados podemos executar todos os tipos de operações básicas necessárias: Save, Update, Delete e Find.

Usando a classe DAO

Teremos apenas uma tabela chamada aluno com os campos: id, nome, login, senha e matricula para o nosso exemplo. Confira o código na Listagem 2.

Listagem 2. DDL para criação da tabela aluno

CREATE TABLE aluno
  (
    id serial NOT NULL,
    nome character varying(200) NOT NULL,
    login character varying(50) NOT NULL,
    senha character varying(50) NOT NULL,
    matricula character varying(50) NOT NULL,
    CONSTRAINT aluno_login_key UNIQUE (login ),
    CONSTRAINT aluno_matricula_key UNIQUE (matricula )
  );

Criaremos um formulário, conforme a Figura 1, que irá realizar as operações de CRUD usando nosso DAO construído anteriormente.

FcadAluno

Figura 1. FcadAluno

Nosso formulário consiste das operações de CRUD básicas:

  • O botão Salvar irá inserir ou atualizar de acordo com a necessidade;
  • O botão remover irá deletar o aluno do banco de dados;
  • O botão buscar irá perguntar ao usuário o login do aluno para que este possa ser carregado no formulário;

Vejamos na Listagem 3 a classe FcadAluno que corresponde ao formulário da Figura 1.

Listagem 3. FcadAluno completo


  import java.sql.ResultSet;
  import java.sql.SQLException;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import javax.swing.JOptionPane;
   
  /**
   *
   * @author ronaldo
   */
  public class FCadAluno extends javax.swing.JFrame {
   
      /**
       * Creates new form FCadUsuario
       */
      public FCadAluno() {
          initComponents();
      }
   
      /**
       * This method is called from within the constructor to initialize the form.
       * WARNING: Do NOT modify this code. The content of this method is always
       * regenerated by the Form Editor.
       */
      @SuppressWarnings("unchecked")
      // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
      private void initComponents() {
   
          jLabel1 = new javax.swing.JLabel();
          jTextField1 = new javax.swing.JTextField();
          jTextField2 = new javax.swing.JTextField();
          jLabel2 = new javax.swing.JLabel();
          jLabel3 = new javax.swing.JLabel();
          jTextField3 = new javax.swing.JTextField();
          jLabel4 = new javax.swing.JLabel();
          jTextField4 = new javax.swing.JTextField();
          jButton1 = new javax.swing.JButton();
          jButton2 = new javax.swing.JButton();
          jButton3 = new javax.swing.JButton();
   
          setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
   
          jLabel1.setText("Nome");
   
          jTextField1.setName("jTextFieldNome"); // NOI18N
   
          jTextField2.setName("jTextFieldLogin"); // NOI18N
   
          jLabel2.setText("Login");
   
          jLabel3.setText("Senha");
   
          jTextField3.setName("jTextFieldSenha"); // NOI18N
   
          jLabel4.setText("Matricula");
   
          jTextField4.setName("jTextFieldMatricula"); // NOI18N
   
          jButton1.setText("Salvar");
          jButton1.setName("jButtonSalvar"); // NOI18N
          jButton1.addActionListener(new java.awt.event.ActionListener() {
              public void actionPerformed(java.awt.event.ActionEvent evt) {
                  jButton1ActionPerformed(evt);
              }
          });
   
          jButton2.setText("Remover");
          jButton2.setName("jButtonRemover"); // NOI18N
          jButton2.addActionListener(new java.awt.event.ActionListener() {
              public void actionPerformed(java.awt.event.ActionEvent evt) {
                  jButton2ActionPerformed(evt);
              }
          });
   
          jButton3.setText("Buscar");
          jButton3.setName("jButtonBuscar"); // NOI18N
          jButton3.addActionListener(new java.awt.event.ActionListener() {
              public void actionPerformed(java.awt.event.ActionEvent evt) {
                  jButton3ActionPerformed(evt);
              }
          });
   
          javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
          getContentPane().setLayout(layout);
          layout.setHorizontalGroup(
              layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
              .addGroup(layout.createSequentialGroup()
                  .addContainerGap()
                  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                      .addComponent(jLabel1)
                      .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE)
                      .addComponent(jLabel2)
                      .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE)
                      .addComponent(jLabel3)
                      .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE)
                      .addComponent(jLabel4)
                      .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE)
                      .addGroup(layout.createSequentialGroup()
                          .addComponent(jButton1)
                          .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                          .addComponent(jButton2)
                          .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                          .addComponent(jButton3)))
                  .addContainerGap(169, Short.MAX_VALUE))
          );
          layout.setVerticalGroup(
              layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
              .addGroup(layout.createSequentialGroup()
                  .addContainerGap()
                  .addComponent(jLabel1)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                  .addComponent(jLabel2)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                  .addGap(18, 18, 18)
                  .addComponent(jLabel3)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                  .addGap(18, 18, 18)
                  .addComponent(jLabel4)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                      .addComponent(jButton1)
                      .addComponent(jButton2)
                      .addComponent(jButton3))
                  .addContainerGap(18, Short.MAX_VALUE))
          );
   
          jTextField1.getAccessibleContext().setAccessibleName("jTextFieldNome");
   
          pack();
      }// </editor-fold>                        
   
      private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         
          
          try {
          String sqlINSERT = "INSERT INTO aluno (nome, login, senha, matricula) values "
                  + "(?,?,?,?)";
          String sqlUPDATE = "UPDATE aluno set nome = ?, senha = ?, matricula = ? WHERE "
                  + "login = ?";
          
          String sqlSELECT = "SELECT count(*) FROM aluno WHERE login = ?";
          
          String nome = jTextField1.getText();
          String login = jTextField2.getText();
          String senha = jTextField3.getText();
          String matricula = jTextField4.getText();
          
          ResultSet rs = MainDAO.getInstance().executeQuery(sqlSELECT, new Object[]{login});
          rs.next();            
          int count = rs.getInt(1);
                 
          if (count > 0){
           MainDAO.getInstance().execute(sqlUPDATE, new Object[]{nome,senha,matricula, login});
           JOptionPane.showMessageDialog(this, "Atualizado com sucesso");
          }else{
          MainDAO.getInstance().execute(sqlINSERT, new Object[]{nome,login,senha,matricula});
          JOptionPane.showMessageDialog(this, "Salvo com sucesso");
          }
          
           } catch (SQLException ex) {
              Logger.getLogger(FCadAluno.class.getName()).log(Level.SEVERE, null, ex);
          }
      }                                        
   
      private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {                                         
              String sqlDELETE = "DELETE FROM aluno WHERE login = ?";
              String login = jTextField2.getText();
              MainDAO.getInstance().execute(sqlDELETE, new Object[]{login});
              JOptionPane.showMessageDialog(this, "Deletado com sucesso");
              jTextField1.setText("");
              jTextField2.setText("");
              jTextField3.setText("");
              jTextField4.setText("");
      }                                        
   
      private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {                                         
          try{
          String login = JOptionPane.showInputDialog("Digite o login do aluno");
          String sqlSELECT = "SELECT * FROM aluno WHERE login = ?";
          ResultSet rs = MainDAO.getInstance().executeQuery(sqlSELECT, new Object[]{login});
          if (rs.next()){
            jTextField1.setText(rs.getString("nome"));
            jTextField2.setText(rs.getString("login"));
            jTextField3.setText(rs.getString("senha"));
            jTextField4.setText(rs.getString("matricula"));
          }else{
            JOptionPane.showMessageDialog(this, "Aluno não encontrado");
          }
          
           } catch (SQLException ex) {
              Logger.getLogger(FCadAluno.class.getName()).log(Level.SEVERE, null, ex);
          }
      }                                        
   
      /**
       * @param args the command line arguments
       */
      public static void main(String args[]) {
          /* Set the Nimbus look and feel */
          //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
          /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
           * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
           */
          try {
              for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                  if ("Nimbus".equals(info.getName())) {
                      javax.swing.UIManager.setLookAndFeel(info.getClassName());
                      break;
                  }
              }
          } catch (ClassNotFoundException ex) {
              java.util.logging.Logger.getLogger(FCadAluno.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
          } catch (InstantiationException ex) {
              java.util.logging.Logger.getLogger(FCadAluno.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
          } catch (IllegalAccessException ex) {
              java.util.logging.Logger.getLogger(FCadAluno.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
          } catch (javax.swing.UnsupportedLookAndFeelException ex) {
              java.util.logging.Logger.getLogger(FCadAluno.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
          }
          //</editor-fold>
   
          /* Create and display the form */
          java.awt.EventQueue.invokeLater(new Runnable() {
              public void run() {
                  new FCadAluno().setVisible(true);
              }
          });
      }
   
      // Variables declaration - do not modify                     
      private javax.swing.JButton jButton1;
      private javax.swing.JButton jButton2;
      private javax.swing.JButton jButton3;
      private javax.swing.JLabel jLabel1;
      private javax.swing.JLabel jLabel2;
      private javax.swing.JLabel jLabel3;
      private javax.swing.JLabel jLabel4;
      private javax.swing.JTextField jTextField1;
      private javax.swing.JTextField jTextField2;
      private javax.swing.JTextField jTextField3;
      private javax.swing.JTextField jTextField4;
      // End of variables declaration                   
  }

Começamos com um construtor fazendo chamada ao método initcomponents(), que é responsável por inicializar todas as variáveis da interface gráfica (labels, texts, buttons e etc) e dispô-las graficamente no formulário, além de inicializar os listeners dos botões, como é o caso do jButton1ActionPerformed() que é chamado dentro do initComponents().

Na última seção, no final da classe, temos o método main(), que possibilita a execução direta do formulário. O método main(), assim como o initComponents() foi gerado automaticamente pela IDE NetBeans, mas fique à vontade para copiá-lo e utilizar em qualquer outra IDE para manter o mesmo estilo da Figura 1.

No botão Salvar temos três comandos SQL: um insert, um Select e um Update. Primeiramente executamos o SELECT através do “MainDAO.getInstance().executeQuery()” para checar se o aluno que possui aquele determinado login já existe na base: caso verdadeiro, então iremos apenas atualizar suas informações usando o UPDATE, caso contrário, iremos inserir as informações através do INSERT.

Perceba que em nenhum momento especificamos qual o banco será usado ou mesmo como se dará a conexão, não nos importa, pois, isso é função do DAO, onde este poderá salvar em qualquer lugar, seja um SDBG ou um arquivo texto.

O próximo botão é o “Remover”, que segue o mesmo princípio do botão “Salvar”, onde criamos um SQL para DELETE. Chamamos o método execute() do nosso MainDAO, e por fim, limpamos os campos que antes estavam preenchidos.

Por fim, o botão “Buscar” tem um JoptionPane, que é acionado para perguntar o login do usuário. Este é procurado através do SELECT e caso ele exista os campos jTextField são preenchidos, caso contrário, uma mensagem de aluno não encontrado é mostrada.

É importante notar neste artigo que o foco principal foi demonstrar um padrão chamado DAO, e como aplicá-lo em um CRUD básico. Normalmente os DAO's atuais são bem mais robustos e completos, não precisando passar comandos SQL para que ele execute o necessário. Quando trabalhamos com frameworks ORM, como é o caso do Hibernate, podemos abstrair os comandos SQL do DAO, exigindo apenas o objeto que será persistido, removido ou buscado.