"O caminho do inferno está pavimentado de boas intenções." -- Marx
Neste capítulo, você aprenderá a:
- Integrar JPA com Spring;
- Injetar o EntityManager dentro do DAO;
- Gerenciar as transações com Spring;
Gerenciando o EntityManager
Dentre as diversas características do Spring uma das que mais chama a atenção é a integração nativa com diversas tecnologias importantes do mercado. Podemos citar o Hibernate e o JPA.
Vimos que o Spring é um container que controla objetos (ou componentes), administrando
o ciclo da vida deles, inclusive ligando uns aos outros (amarrando-os). No caso do JPA, queremos que o Spring cuide
da abertura e do fechamento da EntityManagerFactory
e do EntityManager
. Com isso,
todos os componentes do Spring podem receber como dependência um EntityManager
,
mas agora controlado pelo Spring. Novamente aplicando a inversão de controle.
Mas a inversão de controle não se limita a inicialização de objetos. O Spring também tira
do desenvolvedor a responsabilidade de controlar a transação. Ao delegar o controle do
EntityManager
para o Spring, ele consegue abrir e fechar transações automaticamente.
Veja como é a inicialização do JPA sem a ajuda do Spring:
EntityManagerFactory factory = Persistence.createEntityManagerFactory("tarefas");
EntityManager manager = factory.createEntityManager();
manager.getTransaction().begin();
//aqui usa o EntityManager
manager.getTransaction().commit();
manager.close();
Nesse pequeno trecho de código podemos ver como é trabalhoso inicializar o JPA manualmente.
É preciso abrir e fechar todos os recursos para realmente começar a usar o EntityManager
.
O Spring deve assumir essas responsabilidades e facilitar assim o uso do JPA.
Configurando o JPA no Spring
Para integrar-se com o JPA, o Spring disponibiliza um Bean que devemos cadastrar
no arquivo XML. Ele representa a EntityManagerFactory
, mas agora gerenciada pelo Spring. Ou seja,
toda inicialização da fábrica fica ao encargo do Spring:
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="mysqlDataSource" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
</bean>
Repare que o Bean define Hibernate como implementação do JPA e recebe a mysqlDataSource
que já definimos anteriormente dentro do XML do Spring. Como a nossa Datasource
já sabe os
dados do driver, login e senha, o arquivo persistence.xml
do JPA também fica mais simples:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="tarefas">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>br.com.caelum.tarefas.modelo.Tarefa</class>
<properties>
<!-- SEM as propriedades URL, login, senha e driver -->
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="update" />
</properties>
</persistence-unit>
</persistence>
Injetando o EntityManager
Com JPA configurado podemos aproveitar a inversão de controle e injetar o EntityManager
dentro
de qualquer componente administrado pelo Spring.
Vamos criar uma classe JpaTarefaDao
para usar o EntityManager
.
A classe JpaTarefaDao
precisa do EntityManager
. Como JPA é uma especificação, o Spring
também aproveita uma anotação de especificação para receber a dependência. Nesse caso não podemos usar a anotação @Autowired
do Spring e sim @PersistenceContext
. Infelizmente a anotação
@PersistenceContext
não funciona com construtores, exigindo um outro ponto de injeção. Usaremos
o atributo para declarar a dependência:
@Repository
public class JpaTarefaDao{
@PersistenceContext
private EntityManager manager;
//sem construtor
//aqui vem os métodos
}
Nos métodos do JpaTarefaDao
faremos o uso do EntityManager
usando os métodos já conhecidos
como persist(..)
para persistir uma tarefa ou remove(..)
para remover:
@Repository
public class JpaTarefaDao{
@PersistenceContext
private EntityManager manager;
//sem construtor
public void adiciona(Tarefa tarefa) {
manager.persist(tarefa);
}
public void altera(Tarefa tarefa) {
manager.merge(tarefa);
}
public List<Tarefa> lista() {
return manager.createQuery("select t from Tarefa t").getResultList();
}
public Tarefa buscaPorId(Long id) {
return manager.find(Tarefa.class, id);
}
public void remove(Tarefa tarefa) {
Tarefa tarefaARemover = buscaPorId(tarefa.getId());
manager.remove(tarefaARemover);
}
public void finaliza(Long id) {
Tarefa tarefa = buscaPorId(id);
tarefa.setFinalizado(true);
tarefa.setDataFinalizacao(Calendar.getInstance());
manager.merge(tarefa);
}
}
A implementação do JpaTarefaDao
ficou muito mais simples se comparada com o JdbcTarefaDao
, em alguns
métodos é apenas uma linha de código.
Baixo acoplamento pelo uso de interface
Voltando para nossa classe TarefasController
vamos lembrar que injetamos
o JdbcTarefaDao
para trabalhar com o banco de dados:
@Controller
public class TarefasController {
private final JdbcTarefaDao dao;
@Autowired
public TarefasController(JdbcTarefaDao dao) {
this.dao = dao;
}
@RequestMapping("mostraTarefa")
public String mostra(Long id, Model model) {
model.addAttribute("tarefa", dao.buscaPorId(id));
return "tarefa/mostra";
}
//outros métodos omitidos
}
Quando olhamos para os métodos propriamente ditos, por exemplo, mostra
,
percebemos que o que é realmente necessário para executá-lo é algum DAO.
A lógica em si é independente da instância específica que estamos instanciando,
ou seja para a classe TarefasController
não importa se estamos usando JDBC ou JPA.
Queremos um desacoplamento da implementação do DAO especifico, e algo deve decidir qual
implementação usaremos por baixo dos panos.
O problema desse tipo de chamada é que, no dia em que precisarmos mudar a implementação
do DAO, precisaremos mudar nossa classe. Agora imagine que tenhamos 10 controladores no sistema
que usem nosso JdbcTarefaDao
. Se todas usam a implementação específica,
quando formos mudar a implementação para usar JPA para persistência por exemplo, digamos,
JpaTarefaDao
, precisaremos mudar em vários lugares.
O que precisamos então é apenas uma TarefaDao
dentro da classe TarefasController
,
então vamos definir (extrair) uma nova interface TarefaDao
:
public interface TarefaDao {
Tarefa buscaPorId(Long id);
List<Tarefa> lista();
void adiciona(Tarefa t);
void altera(Tarefa t);
void remove(Tarefa t);
void finaliza(Long id);
}
E a classe JdbcTarefaDao
implementará essa interface:
@Repository
public class JdbcTarefaDao implements TarefaDao {
//implementação do nosso dao usando jdbc
}
Dessa maneira vamos injetar uma implementação compatível com a interface dentro da
classe TarefasController
@Controller
public class TarefasController {
private TarefaDao dao; //usando a interface apenas!
@Autowired
public TarefasController(TarefaDao dao) {
this.dao = dao;
}
//métodos omitidos
}
Agora a vantagem dessa abordagem é que podemos usar uma outra implementação da interface
TarefaDao
sem alterar a classe TarefasController
, no nosso caso vamos usar
JpaTarefaDao
:
@Repository
public class JpaTarefaDao implements TarefaDao{
@PersistenceContext
EntityManager manager;
//sem construtor
//métodos omitidos
}
Repare que o DAO também vai implementar a interface TarefaDao
. Assim temos duas
implementações da mesma interface:
Como o Spring não sabe qual das duas implementações deve ser utilizada é preciso qualificar a dependência:
@Controller
public class TarefasController {
private TarefaDao dao; //usa apenas a interface!
@Autowired
@Qualifier("jpaTarefaDao")
public TarefasController(TarefaDao dao) {
this.dao = dao;
}
//métodos omitidos
}
Dessa maneira o Spring injetará o JpaTarefaDao
.
Gerenciando a transação
Para o suporte à transação ser ativado precisamos fazer duas configurações no
XML do Spring. A primeira é habilitar o gerenciador de transação (TransactionManager
).
Porém, como o Spring pode controlar JPA, Hibernate e outros, devemos
configurar o gerenciador exatamente para uma dessas tecnologias. No caso do JPA, a única
dependência que o JpaTransactionManager
precisa é uma entityManagerFactory
:
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
entityManagerFactory
é o nome do Bean configurado anteriormente:
A segunda parte é avisar que o controle de transações será feito via anotação, parecido com a forma de habilitar o uso de anotações para o Spring MVC.
<tx:annotation-driven/>
Por fim, podemos usar o gerenciamento da transação dentro das nossas classes. Aqui fica muito simples,
é só usar a anotação @Transactional
no método que precisa de uma transação, por exemplo no método
adiciona
da classe TarefasController
:
@Transactional
@RequestMapping("adicionaTarefa")
public String adiciona(@Valid Tarefa tarefa, BindingResult result) {
if(result.hasFieldErrors("descricao")) {
return "tarefa/formulario";
}
dao.adiciona(tarefa);
return "redirect:listaTarefas";
}
A mesma anotação também pode ser utilizada na classe. Isso significa que todos os métodos da classe serão executados dentro de uma transação:
@Transactional
@Controller
public class TarefasController {
Repare aqui a beleza da inversão de controle. Não há necessidade de chamar begin()
, commit()
ou rollback()
, basta usar @Transactional
e o Spring assume a responsabilidade em gerenciar a
transação.
Há um problema ainda, usando o gerenciamento de transação pelo Spring exige a presença do construtor padrão sem parâmetros. Vamos trocar aqui também o ponto de injeção do construtor para o atributo. A classe completa fica como:
package br.com.caelum.tarefas.controller;
//imports
@Controller
@Transactional
public class TarefasController {
@Autowired
TarefaDao dao;
@RequestMapping("novaTarefa")
public String form() {
return "tarefa/formulario";
}
@RequestMapping("adicionaTarefa")
public String adiciona(@Valid Tarefa tarefa, BindingResult result) {
if (result.hasFieldErrors("descricao")) {
return "tarefa/formulario";
}
dao.adiciona(tarefa);
return "tarefa/adicionada";
}
@RequestMapping("listaTarefas")
public String lista(Model model) {
model.addAttribute("tarefas", dao.lista());
return "tarefa/lista";
}
@RequestMapping("removeTarefa")
public String remove(Tarefa tarefa) {
dao.remove(tarefa);
return "redirect:listaTarefas";
}
@RequestMapping("mostraTarefa")
public String mostra(Long id, Model model) {
model.addAttribute("tarefa", dao.buscaPorId(id));
return "tarefa/mostra";
}
@RequestMapping("alteraTarefa")
public String altera(Tarefa tarefa) {
dao.altera(tarefa);
return "redirect:listaTarefas";
}
@RequestMapping("finalizaTarefa")
public String finaliza(Long id, Model model) {
dao.finaliza(id);
model.addAttribute("tarefa", dao.buscaPorId(id));
return "tarefa/finalizada";
}
}
Exercícios: Integrando JPA com Spring
Vamos usar JPA através do Spring. Para isso é preciso copiar os JAR seguintes:
spring-orm-5.2.x.RELEASE.jar
spring-tx-5.2.x.RELEASE.jar
Para isso:
- Vá ao Desktop, e entre no diretório 21/projeto-tarefas/spring jpa.
- Copie os JARs (
CTRL+C
) e cole-o (CTRL+V
) dentro deworkspace/fj21-tarefas/WebContent/WEB-INF/lib
No projeto
fj21-tarefas
, vá a pastaWebContent/WEB-INF
e abra o arquivospring-context.xml
.Nele é preciso declarar o
entityManagerFactory
e o gerenciador de transações.Você pode copiar essa parte do XML do arquivo
Caelum/21/apendice/spring-context-confs.xml
. Copie o conteúdo do arquivo e cole dentro dospring-context.xml
:
<!-- gerenciamento de jpa pelo spring -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="mysqlDataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
</bean>
<!-- gerenciamento da transação pelo spring -->
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven/>
Com essas declarações, Spring gerencia a EntityManagerFactory
e habilita o gerenciamento de transações. Podemos também remover ou comentar as configurações de conexão no persistence.xml
:
<persistence-unit name="tarefas">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>br.com.caelum.tarefas.modelo.Tarefa</class>
<properties>
<!--
<property name="javax.persistence.jdbc.driver"
value="com.mysql.jdbc.Driver" />
<property name="javax.persistence.jdbc.url"
value="jdbc:mysql://localhost/fj21" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="" />
-->
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQL8Dialect" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="update" />
</properties>
</persistence-unit>
- O próximo passo é criar a interface
TarefaDao
. Crie uma nova interface dentro do packagebr.com.caelum.tarefas.dao
:
package br.com.caelum.tarefas.dao;
// imports omitidos
public interface TarefaDao {
Tarefa buscaPorId(Long id);
List<Tarefa> lista();
void adiciona(Tarefa t);
void altera(Tarefa t);
void remove(Tarefa t);
void finaliza(Long id);
}
- Crie uma classe
JpaTarefaDao
que recebe oEntityManager
. Implemente a interfaceTarefaDao
com todos os métodos.
package br.com.caelum.tarefas.dao;
// imports omitidos
@Repository
public class JpaTarefaDao implements TarefaDao{
@PersistenceContext
EntityManager manager;
//sem construtor
public void adiciona(Tarefa tarefa) {
manager.persist(tarefa);
}
public void altera(Tarefa tarefa) {
manager.merge(tarefa);
}
public List<Tarefa> lista() {
return manager.createQuery("select t from Tarefa t")
.getResultList();
}
public Tarefa buscaPorId(Long id) {
return manager.find(Tarefa.class, id);
}
public void remove(Tarefa tarefa) {
Tarefa tarefaARemover = buscaPorId(tarefa.getId());
manager.remove(tarefaARemover);
}
public void finaliza(Long id) {
Tarefa tarefa = buscaPorId(id);
tarefa.setFinalizado(true);
tarefa.setDataFinalizacao(Calendar.getInstance());
}
}
- Agora faça com que a classe
JdbcTarefaDao
implemente a interfaceTarefaDao
, adaptando os métodos existentes caso seja necessário:
@Repository
public class JdbcTarefaDao implements TarefaDao {
//implementação do nosso dao usando jdbc
}
- Altere a classe
TarefasController
, use a interfaceTarefaDao
apenas. Assim a classeTarefasController
fica desacoplado da implementação. Não esquece de apagar o construtor:
@Controller
public class TarefasController {
@Autowired
TarefaDao dao; //usa apenas a interface!
//sem construtor
//métodos omitidos, sem mudança
}
- Como temos dois possíveis candidatos para o processo de injeção que implementam a interface
TarefaDao
o Spring não sabe qual escolher. Temos que adicionar um qualificador na injeção da dependência que dirá qual implementação queremos usar:
@Autowired
@Qualifier("jpaTarefaDao")
TarefaDao dao; //usa apenas a interface!
Por fim, vamos habilitar o gerenciamento de transação para qualquer método da classe
TarefasController
.Abra a classe e use a anotação
@Transactional
(do pacoteorg.springframework.transaction.annotation
) em cima da classe:@Transactional @Controller public class TarefasController {
- Reinicie o Tomcat e acesse a aplicação de tarefas em http://localhost:8080/fj21-tarefas/listaTarefas.
Exercícios Opcionais: Integrando a entidade User com o JPA
- Podemos integrar a entidade User ao JPA, para isso primeiro temos que colocar as anotações necessárias na nossa classe de domínio:
// imports omitidos
@Entity
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// atributos e métodos omitidos
}
- Agora vamos definir uma nova interface chamada
UsuarioDao
:
package br.com.caelum.tarefas.dao;
import br.com.caelum.tarefas.modelo.Usuario;
public interface UsuarioDao {
public boolean existeUsuario(Usuario usuario);
}
- Diremos que a classe
JdbcUsuarioDao
implementa essa interface:
// imports omitidos
@Repository
public class JdbcUsuarioDao implements UsuarioDao {
// métodos omitidos
}
- Agora criaremos a classe
JpaUsuarioDao
que implementará a interface que acabamos de criar:
package br.com.caelum.tarefas.dao;
import org.springframework.stereotype.Repository;
import br.com.caelum.tarefas.modelo.Usuario;
@Repository
public class JpaUsuarioDao implements UsuarioDao {
@Override
public boolean existeUsuario(Usuario usuario) {
// TODO Auto-generated method stub
return false;
}
}
- Precisamos agora adaptar o método
existeUsuario
daJdbcUsuarioDao
para que ele use oEntityManager
. Faremos a injeção doEntityManager
através da anotação@PersistenceContext
, em seguida montaremos uma query buscará no banco de dados uma lista de usuários com um login e uma senha fixa:
@Repository
public class JpaUsuarioDao implements UsuarioDao {
@PersistenceContext
EntityManager manager;
@Override
public boolean existeUsuario(Usuario usuario) {
manager.createQuery("select u from Usuario u "
+ "where u.login = :login and u.senha = :senha", Usuario.class)
.setParameter("login", usuario.getLogin())
.setParameter("senha", usuario.getSenha())
}
}
- Diremos ao JPA que queremos uma lista como resultado, adicionando o méotodo
getResultList()
e capturaremos esse retorno em uma variável chamada listaDeUsuarios:
@Repository
public class JpaUsuarioDao implements UsuarioDao {
@PersistenceContext
EntityManager manager;
@Override
public boolean existeUsuario(Usuario usuario) {
List<Usuario> listaDeUsuarios = manager.createQuery("select u from Usuario u "
+ "where u.login = :login and u.senha = :senha", Usuario.class)
.setParameter("login", usuario.getLogin())
.setParameter("senha", usuario.getSenha())
.getResultList();
}
}
- Se tivermos passado as credenciais corretas de um usuário a lista listaDeUsuarios deverá conter ao menos 1 registro, ou seja, devemos retornar
true
; caso nenhum usuário seja encontrado então a lista é vazia, e retornaremosfalse
. O método completo ficará assim:
@PersistenceContext
EntityManager manager;
@Override
public boolean existeUsuario(Usuario usuario) {
List<Usuario> listaDeUsuarios = manager.createQuery("select u from Usuario u "
+ "where u.login = :login and u.senha = :senha", Usuario.class)
.setParameter("login", usuario.getLogin())
.setParameter("senha", usuario.getSenha())
.getResultList();
if (listaDeUsuarios.isEmpty())
return false;
else
return true;
}
- Agora temos que substuir no
LoginController
a classe pela interface, pedir para o Spring fazer a injeção da dependência e auxiliar ele na escolha de qual implementação da interface deverá ser usada, através da anotação@Qualifier("jpaUsuarioDao")
:
// imports omitidos
@Controller
public class LoginController {
@Autowired
@Qualifier("jpaUsuarioDao")
private UsuarioDao dao;
// métodos omitidos
}
- Caso a tabela
Usuario
não exista, descomente o código comentado empersistence.xml
e adicione a classeUsuario
, de forma semelhante a adição da classeTarefa
- Para cadastrar um novo usuário, no terminal aberto no mysql, use o comando abaixo:
insert into Usuario (login, senha) values('seu_usuario', 'sua_senha');
- Reinicie o Tomcat e tente se logar na aplicação em http://localhost:8080/fj21-tarefas/loginForm.