JPA e Spring framework: criando um DAO Genérico
Veja neste artigo como criar um DAO Genérico utilizando JPA e Spring Framework
Neste artigo iremos mostrar como criar um DAO (Data Access Object) genérico para ser utilizado em qualquer parte do seu sistema, garantindo alta reusabilidade e produtividade. Trabalharemos neste artigo com Spring Framework e Hibernate/JPA para manipulação da nossa classe DAO, porém não iremos mostrar como configurar ou utilizar o Spring ou JPA, pois não é foco deste artigo.
Antes de iniciarmos é importante entender o que vem a ser um DAO, conceito importante para entender o porquê desta classe deve existir em nosso projeto.
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.myapp.dao;
import br.com.sender.bean.AbstractBean;
import java.util.List;
import java.util.Map;
public interface BasicDAO {
public abstract List<? extends AbstractBean>
findByNamedQuery (String s, Map<String, Object> map);
public abstract List<? extends AbstractBean>
findByNamedQuery(String s);
public abstract List<? extends AbstractBean>
findByQuery(String s,
Map<String, Object> map);
public abstract List<? extends AbstractBean>
findByNamedQuery(String s,
Map<String, Object> map, int maxResults);
public abstract List<? extends AbstractBean>
findByNamedQuery(String s,
int maxResults);
public abstract List<? extends AbstractBean>
findByQuery(String s, Map<String, Object>
map, int maxResults);
public abstract AbstractBean save(AbstractBean abstractbean);
public abstract AbstractBean saveFlushAndClear
(AbstractBean abstractbean);
public abstract void update(AbstractBean abstractbean);
public abstract void delete(AbstractBean abstractbean);
public abstract void clear();
public abstract void flushAndClear();
public abstract void flush();
}
Agora que já temos nossa Interface criada, podemos começar a definir nosso DAO Genérico, chamaremos ele 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.
@Repository(value = "basicDAO")
public class BasicDAOImpl implements BasicDAO, Serializable {
@PersistenceContext(type =
javax.persistence.PersistenceContextType.EXTENDED)
protected EntityManager entityManager;
private static Logger logger = Logger.getLogger(BasicDAOImpl.class);
…
…
}
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, colocamos o “PersistenceContextType.EXTENDED” para que nosso entityManager não se torne “detach, assim precisaríamos ficar criando novos a cada momento.
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<? extends AbstractBean>
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<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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 com parâmetros e
* limitando a quantidade máxima
* de resultados que deve ser retornado
* */
@Transactional(readOnly = true, propagation =
Propagation.SUPPORTS)
public List<? extends AbstractBean> findByNamedQuery
(String namedQuery,
Map<String, Object> namedParams, int maxResults) {
try {
logger.info("Procurando pela namedQuery "
+ namedQuery + " com "
+ namedParams.size() + " parametros");
Query query = entityManager.createNamedQuery(namedQuery);
query.setMaxResults(maxResults);
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<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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 003: findByNamedQuery sem parâmetros e nem limitação
* de quantidade, o mais simples de todos
* */
@Transactional(readOnly = true, propagation =
Propagation.SUPPORTS)
public List<? extends AbstractBean> findByNamedQuery
(String namedQuery) {
try {
logger.info((new StringBuilder("Procurando pela namedQuery "))
.append(namedQuery).append(" sem nenhum parametro")
.toString());
Query query = entityManager.createNamedQuery(namedQuery);
List<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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 004: findByNamedQuery sem parâmetros, mas
* com limitação de quantidade de resultados
* */
@Transactional(readOnly = true, propagation =
Propagation.SUPPORTS)
public List<? extends AbstractBean>
findByNamedQuery(String namedQuery,
int maxResults) {
try {
logger.info((new StringBuilder("Procurando pela namedQuery "))
.append(namedQuery).append(" sem nenhum parametro")
.toString());
Query query = entityManager.createNamedQuery(namedQuery);
query.setMaxResults(maxResults);
List<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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");
}
}
Como dissemos logo no início do artigo, 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. Esta classe 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;
}
}
Outro ponto importante em nossos métodos “findNamedQuery()” é o uso de NamedQueries, que são recursos muito uteis 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 {
…
…
...
}
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 alguns 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<? extends AbstractBean> findByQuery
(String hql, Map 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<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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<? extends AbstractBean> findByQuery(String hql,
Map namedParams, int maxResults) {
try {
logger.info((new StringBuilder("Procurando
pela query: ")).append(
hql).toString());
Query query = entityManager.createQuery(hql);
query.setMaxResults(maxResults);
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<? extends AbstractBean> returnList =
(List<? extends AbstractBean>) 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");
}
}
Em suma, a execução e retorno do 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 realização a atualização, inserção e deleção dos registros no banco. Veja na Listagem 7.
@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
public AbstractBean save(AbstractBean bean) {
try {
logger.info("Salvando Bean " + bean.getClass().getName());
entityManager.persist(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 = true, propagation =
Propagation.REQUIRED)
public AbstractBean saveFlushAndClear(AbstractBean bean) {
try {
logger.info("Salvando Bean " + bean.getClass().getName());
entityManager.persist(bean);
entityManager.flush();
entityManager.clear();
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 = true, propagation =
Propagation.REQUIRED)
public void update(AbstractBean bean) {
try {
logger.info("Alterando Bean " + bean.getClass().getName());
entityManager.merge(bean);
} catch (Exception e) {
e.printStackTrace();
logger.error("Ocorreu um erro ao tentar
atualizar. MSG ORIGINAL: "
+ e.getMessage());
throw new DAOException("Ocorreu um erro ao
tentar atualizar");
}
}
@Transactional(readOnly = true, propagation =
Propagation.REQUIRED)
public void delete(AbstractBean bean) {
try {
logger.info("Deletando Bean " +
bean.getClass().getName());
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");
}
}
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.
Temos dois métodos para inserir registros, são eles: save() e saveFlushAndClear(). Para quem já tem mais habilidade com o Hibernate, vai perceber que a diferença entre os dois métodos é que o segundo método, “força” o sincronismo com o banco de dados e a limpeza de alterações na mesma hora que o registro é inserido. 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 o método saveFlushAndClear() faz-se necessário para processar a trigger no mesmo instante e gerar este novo valor.
Concluindo, este artigo teve como principal foco mostrar a estrutura de uma classe DAO genérica, não se atendo a detalhes de utilização do Hibernate ou Spring, caso contrário perderia o foco. 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.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo