Como em todos os projetos, ou pelo menos a maioria deles, é necessário realizar uma conexão e diversos procedimento da aplicação com o banco de dados: consultas, inserções, deleções, alterações e entre outros. Queremos com esse artigo criar uma classe genérica o suficiente para que esses procedimentos possam ser reutilizados em qualquer tipo de projeto, sem a necessidade de ter que recriar tudo de novo, com isso aplicamos aqui o principio da “Reusabilidade”.

Antes de iniciarmos é importante entender o que é um DAO, conceito importante para entender o porque desta classe existir em nosso projeto.

Nota: Vale a pena dar uma conferida nos cursos de java da DevMedia. Se você deseja se tornar um Java Developer de sucesso, você precisa ver esses cursos.

Entendendo o DAO

Conhecido também por Data Access Object ou Objeto de Acesso a Dados, é um padrão para persistência de dados onde seu principal objetivo é separar regras de negócio de regras de acesso a banco de dados. Este é muito utilizado com a arquitetura MVC (Model-View-Controller), onde toda separação de conexões, SQL's e funções diretas ao banco, são tratadas no DAO.

Como iremos tratar de um DAO genérico, este deve ser poderoso o suficiente para realizar quaisquer procedimentos e/ou conexões com nosso banco de dados, deixando que a aplicação nem precise se preocupar com esse tipo de tarefa.

Exemplo: Como estamos trabalhando com Hibernate, podemos fazer uso do HQL (Hibernate Query Language). Sabendo disso, podemos criar um método em nosso DAO chamado findAllByBean(AbstractBean bean), onde passamos qualquer bean e o método nos retorna todos os beans do banco. Veja que neste ponto, não nos importa saber como é feita a conexão ou mesmo o SQL, apenas queremos todos os Beans (Registros) do banco.

O exemplo acima foi apenas para demonstrar que o nosso DAO é quem conversará com o banco de dados (independente de qual for ele), e nossa aplicação conversará com o DAO quando precisar de algum registro do Banco.

Implementando um DAO Genérico

A primeira etapa para começarmos a implementação de nosso DAO Genérico é a criação de uma Interface, pois mesmo que nosso DAO seja super completo com vários recursos ótimos, pode ainda existir algum ponto da aplicação que precise criar seu próprio DAO, um Custom DAO. Nesse caso precisamos garantir que este novo DAO tenha pelo menos os mesmos métodos do nosso DAO Genérico com mais os métodos customizados. Assim, garantimos a organização adequada de nossa aplicação, pois saberemos, por exemplo, que em todos os nossos DAO a operação de salvar um Bean chama-se “save()”, mesmo que com procedimentos internos distintos. Observe a Listagem 1.


package br.com.mygenericdao.dao;
import java.util.List;
import java.util.Map;
import br.com.mygenericdao.bean.AbstractBean;
 
public interface BasicDAO {
 
       public abstract List findByNamedQuery(String s,
                    Map<String, Object> map);
 
       public abstract List findByNamedQuery(String s);
 
       public abstract List findByQuery(String s);
 
       public abstract List findByQuery(String s,
                    Map<String, Object> map);
       
       public abstract AbstractBean findById(Class clazz, Integer id);
 
       public abstract AbstractBean save( AbstractBean bean);
 
       public abstract void delete( AbstractBean bean);
 
       public abstract void clear();
 
       public abstract void flush();
       
       public abstract void evict( AbstractBean bean);
}
Listagem 1. Interface BasicDAO

Agora que já temos nossa Interface criada, podemos começar a definir nosso DAO Genérico, chamado de BasicDAOImpl, por ser uma implementação da nossa Interface acima. Mostraremos primeiro a estrutura do nosso DAO, sem nenhum método, conforme a Listagem 2.


package br.com.mygenericdao.dao;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
 
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
 
import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
 
import br.com.mygenericdao.bean.AbstractBean;
import br.com.mygenericdao.exception.DAOException;
@Repository(value = "basicDAO")
public class BasicDAOImpl implements BasicDAO, Serializable {
 
    /**
        *
        */
    private static final long serialVersionUID = 1L;
        
    @PersistenceContext
    protected EntityManager entityManager;
 
    private static Logger logger = Logger.getLogger(BasicDAOImpl.class);
…
…
}
Listagem 2. Estrutura do BasicDAOImpl

Perceba que anotamos nosso DAO com Spring, utilizando o @Repository. Assim, deixamos o Spring gerenciar a instância da nossa classe. Além disso, temos dois atributos importantes na nossa classe: O entityManager e o logger.

O atributo entityManager é responsável por realizar as conexões e operações com o banco através do JPA. Deixamos o padrão do PersistenceContext como TRANSIENT. Está implícito, mas poderíamos adicionar o atributo.

O atributo logger é apenas para logarmos as operações que estão ocorrendo durante a aplicação. Neste caso, estamos utilizando o Log4j.

Na Listagem 3 você verá nossos métodos findByNamedQuery com diversos tipos de parâmetros, um para cada tipo de situação.


/**
  * OPÇÃO 001: findByNamedQuery com parâmetros
  * */
 @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
 public List findByNamedQuery(String namedQuery,
              Map<String, Object> namedParams) {
       try {
              logger.info("Procurando pela namedQuery " + namedQuery + " com "
                            + namedParams.size() + " parametros");

              Query query = entityManager.createNamedQuery(namedQuery);

              if (namedParams != null) {
                     Entry<String, Object> mapEntry;

                     for (Iterator it = namedParams.entrySet().iterator(); it
                                  .hasNext(); query.setParameter(
                                  (String) mapEntry.getKey(), mapEntry.getValue())) {

                            mapEntry = (Entry<String, Object>) it.next();
                            logger.info("Param: " + mapEntry.getKey() + ", Value: "
                                         + mapEntry.getValue());
                     }

              }

              List returnList = query.getResultList();
              logger.info("Objetos Encontrados: " + returnList.size());

     return returnList;
 } catch (Exception e) {
     e.printStackTrace();
     logger.error("Ocorreu um erro ao executar o findByNamedQuery com parâmetros. MSG ORIGINAL: "
           + e.getMessage());
     throw new DAOException(
           "Ocorreu um erro ao executar o findByNamedQuery com parâmetros");
 }
}


 
 /**
  * OPÇÃO 002: findByNamedQuery sem parâmetros
  * */
 @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List findByNamedQuery(String namedQuery) {
 try {
     logger.info((new StringBuilder("Procurando pela namedQuery "))
           .append(namedQuery).append(" sem nenhum parametro")
           .toString());
     Query query = entityManager.createNamedQuery(namedQuery);
     List returnList = query.getResultList();

     logger.info("Objetos Encontrados: " + returnList.size());

     return returnList;
 } catch (Exception e) {
     e.printStackTrace();
     logger.error("Ocorreu um erro ao executar o findByNamedQuery sem parâmetros. 
     MSG ORIGINAL: "
           + e.getMessage());
     throw new DAOException(
           "Ocorreu um erro ao executar o findByNamedQuery sem parâmetros");
 }
}

/**
  * OPÇÃO 003: find sem namedQuery e sem parametros
  * */
 @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List findByQuery(String s) {
 try {
     logger.info((new StringBuilder("Procurando pela query: "))
           .append(s).toString());
     Query query = entityManager.createQuery(s);
     List returnList = query.getResultList();

     logger.info("Objetos Encontrados: " + returnList.size());

     return returnList;
 } catch (Exception e) {
     e.printStackTrace();
     logger.error("Ocorreu um erro ao executar o findByQuery. MSG ORIGINAL: "
           + e.getMessage());
     throw new DAOException("Ocorreu um erro ao executar o findByQuery");
 }
}

/**
  * OPÇÃO 004: find sem namedQuery e co parametros
  * */
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List findByQuery(String hql, Map<String, Object> namedParams) {
 try {
     logger.info((new StringBuilder("Procurando pela query: ")).append(
           hql).toString());
     Query query = entityManager.createQuery(hql);
     if (namedParams != null) {
       Entry mapEntry;
       for (Iterator it = namedParams.entrySet().iterator(); it
               .hasNext(); query.setParameter(
               (String) mapEntry.getKey(), mapEntry.getValue())) {
           mapEntry = (Entry) it.next();
           logger.info("Param: " + mapEntry.getKey() + ", Value: "
                  + mapEntry.getValue());
       }

     }
     List returnList = query.getResultList();

     logger.info("Objetos Encontrados: " + returnList.size());

     return returnList;
 } catch (Exception e) {
     e.printStackTrace();
     logger.error("Ocorreu um erro ao executar o findByQuery. MSG ORIGINAL: "
           + e.getMessage());
     throw new DAOException("Ocorreu um erro ao executar o findByQuery");
 }

}

/**
  * OPÇÃO 005: find objeto pela classe
  * */

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public AbstractBean findById(Class clazz, Integer id) {
 try {
     logger.info("Procurando pelo Bean " + clazz.getName() + " com ID "
           + id);
     Query query = entityManager.createQuery("SELECT c FROM " + clazz.getName() 
     + " c WHERE c.id = :id");
     query.setParameter("id", id);
     AbstractBean resultBean = (AbstractBean) query.getSingleResult();

     if (resultBean != null)
       logger.info("Objetos Encontrados: 1");
     else
       logger.info("Objetos Encontrados: 0");

     return resultBean;
 } catch (Exception e) {
     e.printStackTrace();
     logger.error("Ocorreu um erro ao executar o findById. MSG ORIGINAL: "
           + e.getMessage());
     throw new DAOException(
           "Ocorreu um erro ao executar o findById");
 }
}
Listagem 3. Métodos findByNamedQuery

Não entraremos no mérito de explicar como utilizar o JPA passo-a-passo. Perceba que temos quatro tipos distintos de métodos, com o mesmo nome, mas parametrizações distintas. Além disso, todos os nossos métodos acima tem a anotação @Transactional(readOnly = true, propagation = Propagation.SUPPORTS), assim, estamos dizendo ao Spring que se houver alguma transação quando esta consulta for realizada, ela deverá fazer parte da transação, caso contrário, ela será executada sem nenhuma transação, diferente do Propagation.REQUIRED, que força a criação de uma transação caso não exista.

Outro ponto importante é o uso da classe AbstractBean, que nada mais é do que do que uma classe Genérica para todos os nossos Beans, tendo apenas a propriedade ID que é comum a todos. Veja como é nossa classe AbstractBean na Listagem 4.


@MappedSuperclass
public abstract class AbstractBean implements Serializable {

   @Id
   @GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY)
   private Integer id;

   public Integer getId() {
         return id;
   }

   public void setId(Integer id) {
         this.id = id;
   }

   @Override
   public int hashCode() {
         final int prime = 31;
         int result = 1;
         result = prime * result + ((id == null) ? 0 : id.hashCode());
         return result;
   }

   @Override
   public boolean equals(Object obj) {
         if (this == obj)
                return true;
         if (obj == null)
                return false;
         if (getClass() != obj.getClass())
                return false;
         
         return (obj instanceof AbstractBean) ? (this.getId() == null ? this == obj : 
         this.getId().equals(((AbstractBean)obj).getId())):false;
         
   }

   
}
Listagem 4. AbstractBean

Outro ponto importante em nossos métodos findNamedQuery() é o uso de NamedQueries, que são recursos muito úteis do JPA. Estas são anotadas diretamente no nosso bean. Veja um exemplo na Listagem 5 de um NamedQuery mapeada em um bean Pessoa.


@Entity
@NamedQueries(value = { @NamedQuery(name = "Pessoa.findAllCompleto", query 
= "SELECT c FROM Pessoa c "
             + "JOIN FETCH c.situacao") })
@Table(name = "pessoa")
@Inheritance(strategy = javax.persistence.InheritanceType.JOINED)
public class Pessoa extends AbstractBean {
…
…
...
}
Listagem 5. NamedQuery no Bean Pessoa

Então quando passarmos uma namedQuery para nosso DAO, apenas passaremos Pessoa.findAllCompleto e o DAO saberá qual query desejamos executar. Simples e funcional. Toda nossa aplicação trabalhará com o conceito de NamedQuery, assim centralizamos nossas Querys e saberemos exatamente onde os erros poderão ocorrer.

Mas, para deixar nosso DAO ainda mais completo, criaremos um “punhado” de métodos para executar outras Querys que não sejam NamedQueries. Por algum motivo poderemos executar querys diretas sem o uso desse recurso do JPA. Vejamos como na Listagem 6.


@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List findByQuery(String s) {
   try {
       logger.info((new StringBuilder("Procurando pela query: "))
             .append(s).toString());
       Query query = entityManager.createQuery(s);
       List returnList = query.getResultList();

       logger.info("Objetos Encontrados: " + returnList.size());

       return returnList;
   } catch (Exception e) {
       e.printStackTrace();
       logger.error("Ocorreu um erro ao executar o findByQuery. MSG ORIGINAL: "
             + e.getMessage());
       throw new DAOException("Ocorreu um erro ao executar o findByQuery");
   }
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List findByQuery(String hql, Map<String, Object> namedParams) {
   try {
       logger.info((new StringBuilder("Procurando pela query: ")).append(
             hql).toString());
       Query query = entityManager.createQuery(hql);
       if (namedParams != null) {
         Entry mapEntry;
         for (Iterator it = namedParams.entrySet().iterator(); it
                 .hasNext(); query.setParameter(
                 (String) mapEntry.getKey(), mapEntry.getValue())) {
             mapEntry = (Entry) it.next();
             logger.info("Param: " + mapEntry.getKey() + ", Value: "
                    + mapEntry.getValue());
         }

       }
       List returnList = query.getResultList();

       logger.info("Objetos Encontrados: " + returnList.size());

       return returnList;
   } catch (Exception e) {
       e.printStackTrace();
       logger.error("Ocorreu um erro ao executar o findByQuery. MSG ORIGINAL: "
             + e.getMessage());
       throw new DAOException("Ocorreu um erro ao executar o findByQuery");
   }

}
Listagem 6. Métodos sem uso de NamedQueries

Em suma, a execução e retorno dos método findByNamedQuery() e findByQuery() são iguais, o que os diferencia é apenas que no primeiro caso temos como parâmetro uma NamedQuery mapeada em nosso Bean e no segundo caso temos um HQL “puro” passado como parâmetro.

Por fim, temos os métodos que realizarão a atualização, inserção e deleção dos registros no banco. Veja na Listagem 7.


@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public AbstractBean save(AbstractBean bean, boolean flushAndRefresh) {
   try {
       logger.info("Salvando Bean " + bean.getClass().getName());
       bean = entityManager.merge(bean);

       if (flushAndRefresh) {
         entityManager.flush();
         entityManager.refresh(bean);
       }
       
       return bean;

   } catch (Exception e) {
       e.printStackTrace();
       logger.error("Ocorreu um erro ao tentar salvar. MSG ORIGINAL: "
             + e.getMessage());
       throw new DAOException("Ocorreu um erro ao tentar salvar");
   }
}

@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public AbstractBean save(AbstractBean bean) {
   return save(bean, false);
}


@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public void delete(AbstractBean bean) {
   try {
       logger.info("Deletando Bean " + bean.getClass().getName());
       bean = entityManager.merge(bean);
       entityManager.remove(bean);
   } catch (Exception e) {
       e.printStackTrace();
       logger.error("Ocorreu um erro ao tentar deletar. MSG ORIGINAL: "
             + e.getMessage());
       throw new DAOException("Ocorreu um erro ao tentar deletar");
   }
}

public void evict(AbstractBean bean){
   Session session = (Session) entityManager.getDelegate();
   session.evict(bean);
}

public void flush() {
   entityManager.flush();
}

public void clear() {
   entityManager.clear();
}
Listagem 7. Métodos de alteração, inserção e remoção de registros

Perceba primeiramente que todos os nossos métodos da Listagem 7 tem o Propagation.REQUIRED, ou seja, eles obrigatoriamente sempre estarão em uma transição para serem executados.

Adicionamos ainda mais três métodos importantes: evict, flush e clear.

  1. O evict limpa um determinado objeto (de acordo com o passado pelo parâmetro do método) do persistenceContext atual.
  2. O flush sincroniza todas as alterações com o banco de dados, ou seja, ele efetiva as alterações que atualmente estão salvas no persistenceContext no banco de dados.
  3. O clear faz a mesma função do evict com a única diferença que o clear limpa todos os objetos do persistenceContext e não apenas um como o evict faz.

O nosso save na verdade realiza uma 'merge', ou seja, ele pode tanto inserir ou atualizar. Isso vai depender se o objeto está ou não no banco de dados.

Para quem já tem mais habilidade com o Hibernate ou mesmo JPA, vai perceber que a diferença entre executar o método passando a variável flushAndRefresh como true. É que este “força” o sincronismo com o banco de dados e depois repopula o objeto no persistenceContext, através do refresh. Imagine, por exemplo, se você possuir uma trigger que é executada a cada vez que um registro é inserido, atualizando o valor de uma coluna qualquer, aqui a variável flushAndRefresh como true faz-se necessária para processar a trigger no mesmo instante e gerar este novo valor.

A utilização do JPA contribui ainda mais para abstrair tecnologias, sendo assim, você pode optar por utilizar outro Container que não seja o Hibernate, sem influenciar na aplicação. Adotamos ainda o princípio e boa prática de utilizar interfaces para manter nossa aplicação mais organizada, legível e bem estruturada.