Escolar Documentos
Profissional Documentos
Cultura Documentos
Sumário
2 Definição do projeto 4
2.1 Descrição do problema 4
2.2 Tecnologias escolhidas 4
2.3 Uma visão geral sobre o domínio do projeto 5
2.4 Exercícios: Criando o projeto no eclipse 5
6 Introdução a Java EE 60
6.1 Gerenciamento do JPA 60
6.2 Inversão de Controle 62
6.3 Java EE - Java Enterprise Edition 63
6.4 Servidor de aplicação 64
6.5 Para saber mais: JBoss AS se chama WildFly 66
6.6 Exercícios: Instalação do JBoss AS 10 - Wildfly 66
6.7 Diretórios do WildFly 66
6.8 Exercícios: Configurando o WildFly no WTP do Eclipse 67
6.9 Em casa 74
Versão: 20.5.21
CAPÍTULO 1
"Eu respeito o poder eterno, não cabe a mim determinar seus limites, eu não afirmo nada, contento-me em
crer que sejam possíveis mais coisas do que nós pensamos." -- Voltaire, Micromegas, considerado o
primeiro conto de ficção científica da história
As ferramentas de persistência são hoje populares não apenas na plataforma Java. Quais seriam os
atrativos dessas ferramentas? Como tirar o maior proveito delas?
Há o outro lado da moeda: utilizar a JPA2 sem se preocupar um pouco com alguns detalhes de
mapeamento, lazy loading e cache, vai certamente gerar um número excessivo de queries, podendo
facilmente derrubar um banco de dados. Veremos que a JPA2 vai bem além da geração de queries: ela
gerencia o estado de persistência dos objetos, de tal modo que cuidará de detalhes muito além de simples
queries, como o cache, tipo de query a ser realizada, estratégia de persistência e transação, e muito mais.
E a relação da JPA2 com o Hibernate? Por qual optar? É importante saber que a JPA2 é apenas uma
especificação, uma série de interfaces, que precisamos de uma implementação (JPA Provider) para poder
utilizá-la. No curso, esse provider será o Hibernate, mas nada impede que você use outros providers,
como o Apache OpenJPA ou o EclipseLink.
Iremos ver, além das características importantes e que usamos sempre na JPA2, detalhes do dia a dia
que são essenciais para o bom funcionamento de sua aplicação, evitando os erros comuns que podem
causar problemas de performance e escalabilidade.
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Fora isso, sinta-se à vontade para entrar em contato com seu instrutor e tirar todas as dúvidas que
tiver durante o curso.
Dentre os cursos mais avançados, o FJ-36 aborda tópicos do Java EE para sistemas ainda mais
robustos, abordando RMI, JNDI, EJB, JMS e Web Services. O FJ-91, conhecido curso de Arquitetura e
Design da Caelum, vai atacar inúmeras escolhas que podemos tomar durante a criação de uma aplicação,
mostrando vantagens e desvantagens de cada framework, de cada decisão de design, protocolo etc.
CAPÍTULO 2
DEFINIÇÃO DO PROJETO
Para facilitar o acesso dos usuários, o sistema será Web, sendo possível utilizá-lo de qualquer lugar
para manipular as contas e suas transações.
Uma característica importante do sistema é que as movimentações realizadas dentro de cada conta
poderão ser categorizadas, para que possam ser facilmente agrupadas e pesquisadas posteriormente.
4 2 DEFINIÇÃO DO PROJETO
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
SISTEMA WEB
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
CAPÍTULO 3
"Eu não vou te amar: eu te conheço demais para isso" -- Jean Paul Sartre em Huis Clos (Entre quatro
paredes)
Conseguimos trabalhar com o banco de dados criando tabelas, editando e consultando informações
através do SQL (Structured Query Language), que, apesar de ter um padrão ANSI, apresenta diferenças
significativas dependendo do fabricante.
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Note que há outras linguagens, além do Java, que suportam o paradigma de orientação a objetos,
como C#, Ruby e Python.
Para resolver esse problema, surgiram algumas estratégias, como remover os SQLs da aplicação,
extraindo-os para um arquivo externo, como um txt ou xml , por exemplo. No entanto, esse
mecanismo deveria ser desenvolvido para cada projeto e diferentes estratégias existiam para os diferentes
projetos existentes. Frameworks surgiram para fazer esse isolamento, como o IBatis.
O ideal seria que trabalhássemos apenas com a nossa linguagem de programação e todo o trabalho
do banco de dados que envolve SQL, transação e outros cuidados nos fossem abstraídos. Ou seja, para
"gravarmos" um objeto no banco de dados, queremos utilizar o mínimo de código possível, tal como:
conexao.salva(conta);
O método salva da conexão se encarregaria de fazer todo o trabalho pesado para gerar o SQL
adequado que será disparado no banco de dados, assim como gerenciaria as necessidades de conexão e
transação. No fundo, o que queremos é diminuir a necessidade do uso de detalhes do paradigma
relacional e programarmos mais focado com o paradigma orientado a objetos em mente. Mais tempo
para nos dedicarmos a lógica de negócios.
O que precisamos é trabalhar com esses dois paradigmas para conseguirmos programar apenas
orientado a objetos e toda a complexidade da impedância entre os dois modelos esteja abstraída de nós
desenvolvedores.
Temos que, de alguma forma, converter o que existe no mundo orientado a objetos para o relacional.
Como fazer? Será que é trivial?
O primeiro passo é mapear as diferenças entre os modelos. Como é muito comum no Java, já
existem bibliotecas prontas que fazem esse mapeamento para nós, nesse caso, os famosos frameworks
ORM (Object Relational Mapping).
Persistence API), uma API uniforme que todos os frameworks ORM deveriam implementar para serem
reconhecidos pela Sun.
Como toda especificação, ela deve possuir implementações. Entre as mais comuns, podemos citar:
Hibernate, EclipseLink e Apache OpenJPA.
Para este curso, usaremos a versão 2.1 da JPA, a mais recente, que foi lançada oficialmente em maio
de 2013 junto com o Java EE 7. A implementação que vamos utilizar é o Hibernate, por ser líder de
mercado e opensource.
É fundamental entender que a JPA é uma especificação, um PDF que define uma série de
interfaces e o que deve acontecer após a invocação de cada método. A JPA 2.1 é definida pela JSR
338 e faz parte do Java EE 7:
http://jcp.org/en/jsr/detail?id=338
Já que JPA2 é uma especificação, você poderá aproveitar todo o conhecimento aqui adquirido se
quiser usar o EclipseLink ou quaquer outro provider, bastando trocar a configuração.
Também veremos algumas características mais avançadas que são dependentes de provider, isto é,
será diferente a forma de fazer em cada provider. Detalhes de estatísticas, pooling e configurações
especiais de cache vão variar bastante, como veremos nos capítulos mais avançados.
Selecione a versão do Hibernate que você deseja utilizar no projeto. Neste treinamento utilizaremos a
última versão estável para a JPA 2.1, a versão 5.2.6.Final.
Caso seja perguntado se deseja fazer login com uma conta da Oracle, escolha a opção No thanks, just
start my download.
Após os downloads, basta extrair os arquivos que estão dentro do pacote da distribuição do
Hibernate e o arquivo mysql-connector-java-5.1.xx-bin.jar do pacote do MySQL. Para nossa
aplicação, vamos precisar dos .jars que estão dentro da pasta lib/required do Hibernate e do .jar
do driver do MySQL. Vamos adicioná-los ao classpath da sua aplicação:
Depois, copie somente os .jars citados anteriormente para ela. Não esqueça de dar um
Refresh no projeto ao voltar para o eclipse:
Abra a pasta lib , selecione todos os .jars , clique com o botão direito do mouse sobre as
seleções e escolha Build Path > Add to Build Path :
Essa classe deverá guardar o número da conta, agência, banco e nome do titular:
package br.com.caelum.financas.modelo
Perceba que a classe conta não tem nada de especial. Ele é um objeto simples Java, um JavaBean, um
POJO.
Vale relembrar que devemos sempre utilizar getters e setters com cuidado: só os criamos de acordo
com a real necessidade. Por exemplo, talvez faça mais sentido, em alguns casos e projetos, ter métodos
como deposita e saca em vez de setSaldo . Veja mais em: http://blog.caelum.com.br/nao-
aprender-oo-getters-e-setters/
Precisamos dizer que a nossa classe Conta será gerenciada pela implementação da JPA que
decidimos utilizar e assim ter sua representação no banco de dados. Para dizermos isso à JPA, basta
utilizarmos a anotação @Entity sobre a classe Conta :
@Entity
public class Conta {
// ...
}
Um detalhe importante sobre a anotação @Entity é que ela vem do pacote javax.persistence ,
o que significa que ela é da especificação JPA, ou seja, ao utilizarmos a anotação já estamos utilizando a
JPA.
Todas as entidades que criarmos para trabalhar com a JPA devem possuir, além da anotação
@Entity , uma indicação de qual atributo representará sua chave primária. Para isso devemos criar um
atributo que represente essa chave. Vamos utilizar um atributo do tipo Integer para representar uma
chave que será um id . Ele deverá estar anotado com @Id :
@Entity
public class Conta {
@Id
private Integer id;
Somente isso já seria o suficiente para mapearmos a nossa entidade. Dessa forma, no momento em
que precisarmos persistir essa conta no banco de dados, a JPA esperará uma tabela com as seguintes
características: campos texto (varchar, text, etc) para titular, agencia e numero, e um campo numérico
para id. O tipo não precisa ser exato, apenas compatível para as queries.
No entanto, repare que em nenhum momento explicitamos o nome da tabela, ou que queríamos os
campos titular , agencia e numero nas nossas tabelas. Então como ele descobriu isso? Que regra
ele usa para saber quais campos a tabela deveria ter? A JPA segue a ideia de que não precisamos
configurar todos os pequenos detalhes e para isso ela já vem com diversas convenções dentro dela. Essas
convenções dizem que o nome da tabela é o nome da classe da entidade e também que todos os atributos
da sua entidade se tornarão colunas da sua tabela.
Esse conceito é conhecido como Convention over Configuration e é muito comum em frameworks
MVC e ORM.
E se não quisermos a convenção? E se quiséssemos outro nome para a tabela? Sempre que a
convenção não nos servir, podemos fazer configuração, e na JPA essa configuração é sempre feita
usando anotações. Por exemplo, no caso de querermos outro nome de tabela, bastaria anotar a nossa
classe Conta com a anotação @Table indicando no parâmetro name qual nome ela deveria ter:
@Table(name="tb_contas_bancarias")
@Entity
public class Conta {
// ...
}
Uma outra convenção da JPA é com relação ao valor da chave primária. Por padrão, a JPA espera
que o atributo anotado com @Id receba manualmente um valor. No entanto, isso acaba se tornando
bastante trabalhoso muitas vezes, pois precisaríamos gerenciar os valores possíveis para a nossa chave
primária tomando o cuidado de não permitir repetições. Uma solução melhor para isso é deixar o
próprio banco de dados gerar os valores para a chave primária. Muitos bancos de dados possuem
suporte a geração automática de chaves primárias, seja por auto incremento ou sequências.
Para dizermos à JPA que queremos que o banco de dados cuide dessa geração bastaria adicionarmos
à nossa chave primária a configuração adequada. Nesse caso, teríamos que adicionar a anotação
@GeneratedValue no atributo id :
@Entity
public class Conta {
@Id
@GeneratedValue
private Integer id;
Com o decorrer do curso aprenderemos a customizar os nossos modelos e quais anotações devemos
usar em cada caso.
Estratégia (ou strategy) é o nome que se dá às diferentes formas de implementar uma chave cujo
conteúdo é gerenciado pelo próprio banco de dados.
Especificando a estratégia como IDENTITY forçamos a JPA a escolher utilizar colunas com
valores auto incrementáveis. Atente-se ao fato de que alguns bancos de dados podem não
suportar essa opção (por exemplo PostgreSQL e Oracle).
Nesse caso, estaremos configurando nossa chave para trabalhar com uma SEQUENCE do
banco de dados. O comportamento padrão nesse caso é uma SEQUENCE global para a
aplicação, onde todas as tabelas compartilhariam uma mesma sequência. Por exemplo, ao
salvar uma Conta ela receberia id 1, depois salvando uma Movimentacao ela receberia o
id 2, e ao salvar outra Conta ela receberia id 3. Em diversos casos, essa abordagem não
chega a ser um problema, mas podemos querer especificar um gerador de chave específico
para cada tabela; ou ainda deixar tabelas simples com o gerador global e algumas específicas
com um gerador individual. Para fazer essa configuração, basta darmos um nome diferente
para cada gerador de ids que desejarmos, através do atributo generator na anotação
@GeneratedValue .
@Entity
public class Conta {
@SequenceGenerator(name = "contaGenerator",
sequenceName = "CONTA_SEQ", allocationSize = 10)
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "contaGenerator")
private Integer id;
// outros atributos e métodos
}
@Entity
public class Movimentacao {
@SequenceGenerator(name = "movimentacaoGenerator",
sequenceName = "MOVIMENTACAO_SEQ", allocationSize = 10)
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "movimentacaoGenerator")
private Integer id;
// outros atributos e métodos
}
Dessa forma, teremos cada entidade seguindo uma sequência de id própria. Vimos ainda a
anotação @SequenceGenerator , anotação usada para especificar alguns detalhes da sequência, como o
nome e sua allocationSize , que serve pra definir de quantos em quantos números a SEQUENCE sera
chamada. Por exemplo, acima definimos 10, isso significa que a SEQUENCE será chamada pela primeira
vez e devolverá 1, mas já serão buscados 10 números. Para os próximos 9 ids , eles serão buscados da
memória, e só depois do décimo a SEQUENCE será chamada novamente. Isso evita diversas idas ao
banco para gerar a chave. Se não especificarmos nada, o tamanho padrão do allocationSize é 50.
Uma outra opção interessante para o @SequenceGenerator é a definição do valor inicial da sequência,
através do atributo initialValue .
Assim como acontece com a estratégia 'IDENTITY', alguns bancos de dados podem não suportar
esta opção (como por exemplo o MySQL que usaremos durante o curso)
table = "GENERATOR_TABLE",
pkColumnValue = "CONTA")
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator="CONTA_GENERATOR")
private Integer id;
// outros atributos e métodos
}
Quando utilizamos a estratégia Table , é necessária uma tabela que vai gerenciar as chaves da nossa
entidade. Podemos ter uma mesma tabela para todo o sistema, que vai diferenciar o dono da chave pelo
conteúdo do atributo pkColumnValue da @TableGenerator . Podemos também ter mais de uma
tabela de controle. O único atributo obrigatório é o nome do gerador, todo o resto podemos não
especificar e deixar a convenção fazer seu trabalho.
Para mais algumas informações de como a JPA lida com o mapeamento de chaves simples e
compostas, você pode consultar o livro Aplicações Java para a Web com JSF e JPA editado pela
Casa do Código (www.casadocodigo.com.br)
O tipo AUTO não chega a ser uma estratégia real, ela serve para deixar a implementação da
JPA decidir a melhor estratégia dependendo do banco utilizado, tendo assim uma maior
portabilidade. Além disso, quando não definimos a estratégia, esse é o valor utilizado por
padrão.
A partir da versão 5.0 do Hibernate, ao utilizar a estratégia AUTO , caso o banco de dados suporte a
estratégia SEQUENCE , esta será utilizada. Caso contrário, a estratégia TABLE será a escolhida pelo
framework. No caso do EclipseLink, a estratégia escolhida é sempre a TABLE .
Os dados que vão nesse arquivo são divididos entre detalhes específicos da implementação, no caso o
Hibernate, e alguns que são padrões para qualquer implementação de JPA2 que viermos a utilizar.
Vamos começar com as configurações básicas e depois veremos detalhes mais avançados como controle
de cache, connection pool, validação etc.
Para nosso sistema, inicialmente, temos que informar que a implementação que vamos utilizar é o
Hibernate e quais classes devem ser gerenciadas por ele, mesmo tendo anotado nosso modelo,
precisamos indicar novamente nesse arquivo. Além disso, temos as configurações que são obrigatórias
para qualquer aplicação Java que vá conectar a um banco de dados, e que já conhecemos do JDBC: string
de conexão com o banco, o driver, o usuário e senha. Precisamos também informar qual dialeto de SQL
deverá ser usado no momento que as queries são geradas, no nosso caso MySQL.
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persi
stence/persistence_2_1.xsd">
<properties>
<!-- Propriedades JDBC -->
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/fj25" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="" />
Outra opção é o create onde o Hibernate remove as tabelas e em seguida as cria a partir das
classes anotadas.
Por fim, também é possível utilizar a opção validate onde é executada apenas uma
verificação para saber se o banco de dados está "sincronizado" com o modelo de classes mas nada é
executado no banco de dados.
No momento em que a EntityManagerFactory é criada, uma das operações que são feitas é a
criação das tabelas. Isso é determinado pela configuração hibernate.hbm2ddl.auto definida no
persistence.xml onde colocamos o valor como update , que significa que o Hibernate tentará fazer
atualizações incrementais no schema do banco de dados de acordo com o modelo das classes.
EntityManagerFactory factory =
Persistence.createEntityManagerFactory("controlefinancas");
manager.getTransaction().begin();
manager.persist(conta);
manager.getTransaction().commit();
manager.close();
<properties>
<!-- Propriedades JDBC -->
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/fj25" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="" />
2. Vamos preparar nosso projeto com as dependências que o Hibernate, como implementação da JPA2,
precisa.
FAZENDO EM CASA
Caso você esteja fazendo esse passo de casa, pode seguir o passo a passo descrito na sessão
Instalando o Hibernate e o driver do MySQL deste mesmo capítulo.
Copie os JARs do Hibernate para a pasta lib do seu projeto dessa forma:
Clique com o botão direito no nome do projeto fj25-financas e escolha New -> Folder. Escolha lib
como nome dessa pasta e pressione Finish.
No Desktop, entre novamente no ícone Atalho para arquivos dos cursos e depois no
diretório 25 ;
Selecione o driver mais novo do MySQL no diretório mysql-driver , clique com o botão direito
e escolha Copy;
Clique novamente da direita sobre as seleções dos .jars , escolha no menu Build Path -> Add to
Build Path;
6. Gere os getters e setters usando o eclipse. (Source -> Generate Getters and Setters ou Ctrl + 3 -> ggas).
Lembrando que devemos gerar getters e setters de acordo com cada caso, mesmo em uma entidade
eles podem não ser necessários.
7. Anote a sua classe como uma entidade de banco de dados. Lembre-se que essa é uma anotação do
pacote javax.persistence :
@Entity
public class Conta {
// atributos e métodos
}
8. Anote seu atributo id como chave primária e como campo de geração automática. É boa prática
também definir a estratégia que o banco usará para gerenciar esse campo. Utilizaremos IDENTITY
como estratégia:
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
1. Crie o banco de dados no MySQL. Para isso abra o terminal e rode os seguintes comandos:
package br.com.caelum.financas.teste;
//imports omitidos
manager.getTransaction().begin();
manager.persist(conta);
manager.getTransaction().commit();
manager.close();
factory.close();
9. Execute o método main criado. Para verificar a inserção, acesse o MySQL via terminal e verifique se a
conta foi, de fato, gravada. Para fazer isso, siga os seguintes passos:
E logo antes de finalizar o método, imprima o tempo total de execução. Para isso, adicione as
seguintes linhas antes do fim do método:
long fim = System.currentTimeMillis();
System.out.println("Executado em: " + (fim - inicio) + "ms");
Execute a classe e veja quanto tempo demora para o método ser executado. Você considera isso um
tempo aceitável para que um simples insert execute? Execute um insert diretamente no banco de
dados usando o terminal e compare os tempos.
2. Remova a declaração da entidade no persistence.xml e tente executar a classe que insere uma
nova Conta no banco de dados. Repare que vai continuar funcionando. Por quê?
3. Teste as outras formas do Hibernate para fazer a criação e evolução do banco de dados. Para isso, no
persistence.xml altere a propriedade hibernate.hbm2ddl para utilizar os valores create ,
create-drop ou validate . Veja as diferenças.
CAPÍTULO 4
"Tudo tem alguma beleza, mas nem todos são capazes de ver" -- Confúcio
Mas, o que será que acontece quando criamos uma EntityManagerFactory ? No capítulo anterior,
verificamos que, ao gravar uma nova conta no banco, o processo é um pouco mais demorado do que
normalmente seria para realizar um insert em um banco de dados. Isso acontece justamente pelo fato de
estarmos criando a EntityManagerFactory . Nesse momento, a implementação da JPA geralmente lê
todas as anotações que foram colocadas nas entidades, cria conexões com o banco de dados, entre outras
operações.
Para que consigamos trabalhar com a JPA2, devemos criar sempre uma EntityManagerFactory ,
mas não precisamos criá-la a todo instante que precisarmos salvar um novo objeto. Portanto,
precisamos, de alguma forma, fazer com que EntityManagerFactory seja criada uma única vez no
projeto. Para isso, teremos uma classe chamada JPAUtil . Essa classe se encarregará de criar a
EntityManagerFactory uma única vez, e fará isso com o uso de um atributo estático.
Por fim, a classe JPAUtil terá um método que construirá EntityManager quando for preciso
persistir objetos. Podemos criar um método getEntityManager que pedirá para a factory devolver.
É muito importante saber que um EntityManager deve ter seu ciclo de vida tratado com extremo
cuidado. Ele é um objeto caro: consome recursos importantes, como a conexão do banco, caches, etc. É
fundamental tomar cuidado, como exemplo lembre-se sempre de fechar seus EntityManagers ,
verificar se as transações foram comitadas com sucesso e assim por diante.
No dia a dia o indicado é usar um framework que já tenha filtros para trabalhar com o
EntityManager , muitos deles vão até injetar esse objeto para você, facilitando ainda mais o seu
trabalho, como é o caso do JSF2 pelo CDI. Veremos o pattern open session/entitymanager in view
posteriormente no curso, e seu uso é necessário em muitos casos.
// imports omitidos
2. Altere a classe TestaInsereConta para que ela use a JPAUtil , removendo a criação da
EntityManagerFactory e buscando um EntityManager através de new
JPAUtil().getEntityManager() .
package br.com.caelum.financas.teste;
// imports omitidos
conta.setTitular("Jose Roberto");
conta.setBanco("Banco do Brasil");
conta.setNumero("123456-6");
conta.setAgencia("0999");
manager.getTransaction().begin();
manager.persist(conta);
manager.getTransaction().commit();
manager.close();
Para evitar esse problema, vamos desde o começo encapsular todas as operações que lidarão com o
banco de dados e com a JPA dentro de uma classe, cujo propósito será esconder as operações que serão
feitas com o banco de dados em uma interface mais específica para os nossos problemas.
A classe que encapsulará o código para trabalhar com os objetos persistidos de uma determinada
classe segue um Design Pattern chamado DAO - Data Access Object. Para criarmos um DAO que
encapsulará o acesso aos dados da Conta podemos criar uma classe chamada ContaDao :
O próximo passo é criar o método que faz a gravação da conta no banco de dados. Para isso,
queremos ter algo como:
public class ContaDao {
// atributos aqui...
manager.getTransaction().begin();
manager.persist(c);
manager.getTransaction().commit();
manager.close();
}
}
Mas temos um problema grave ao fazermos isso. Quando abrimos a EntityManager dentro do
método salva , ela só pode ser fechada dentro do mesmo método e, caso não a fechemos, ela ficará
aberta para sempre. O ideal seria que nosso DAO não se preocupasse com abertura de EntityManager s
nem com tratamento de transações.
Para conseguirmos isso, vamos fazer com que nosso DAO dependa de uma EntityManager para
funcionar e sempre que for preciso usar um DAO, deverá ser provido uma EntityManager para ele.
Bastaria declararmos essa dependência no construtor do DAO :
public class ContaDao {
Agora, podemos evoluir o nosso DAO adicionando uma busca pelo id . Para isso, precisamos
invocar o método find na EntityManager passando como parâmetro qual entidade desejamos fazer
Para fazermos a exclusão de alguma Conta , poderíamos invocar o método remove , passando um
objeto com a entidade que deve ser excluída:
public void exclui(Conta conta) {
this.manager.remove(conta);
}
// imports omitidos
public class ContaDao {
Por enquanto, deixaremos o nosso DAO sem o método para fazer a alteração das contas. Antes
de vermos como fazê-lo, precisamos aprender alguns detalhes de como a JPA2 trata o ciclo de vida
de nossos objetos e seus estados.
Isso vai desde nomes de classes até os nomes dos métodos. Essa corrente ficou conhecida como o
Domain Driven Design. Eric Evans, um dos seus maiores evangelizadores, escreveu o livro sobre o
assunto chamado Domain Driven Design - Tackling Complexity From The Heart of Software, que é
considerado por muitos desenvolvedores uma leitura obrigatória.
Um dos conceitos que o Domain Driven Design introduz é a questão de uma linguagem única entre a
modelagem e a forma com que o domínio é tratado pelos especialistas e os desenvolvedores. Essa
linguagem única é conhecida como Ubiquitous Language e deve ser refletida no código no momento do
desenvolvimento. A importância da linguagem ubíqua é que com os desenvolvedores e os especialistas
no domínio se referindo aos mesmos termos, diminui-se o ruído da comunicação entre essas equipes e
como consequência, uma diminuição de falhas por conta de mal compreensão do domínio.
Um outro ponto que é muito discutido é a substituição dos DAOs por objetos que estejam mais
atrelados ao domínio e dessa forma possuam menos acoplamento com a infraestrutura. Eles teriam uma
interface mais voltada ao negócio, com verbos e substantivos que possam ser usado na conversa com os
usuários do sistema. Esses objetos seguem o pattern Repository, como uma Agenda para armazenar
Contato s.
No fundo, o objetivo do Domain Driven Design é que o quanto mais parecido o seu código estiver
com o domínio que está sendo trabalhado, melhor, e para isso existem diversas técnicas.
Indo mais longe, há ainda o pattern Active Record, onde a própria entidade possuiria métodos de
busca, leitura, alteração, como conta.save() . Apesar de muito comum em linguagens dinâmicas, essa
abordagem possui complicações para ser implementada em linguagens estaticamente tipadas como Java,
e não teve muita adoção.
4. Altere a sua classe TestaInsereConta para utilizar o ContaDAO em vez de utilizar diretamente o
EntityManager , substituindo a invocação de persist por adiciona do nosso DAO.
manager.getTransaction().begin();
dao.adiciona(conta);
manager.getTransaction().commit();
manager.close();
}
}
6. Vamos testar a busca por id , para isso crie uma classe chamada TestaPesquisaIdConta no
pacote br.com.caelum.financas.teste :
System.out.println(encontrado.getTitular());
manager.close();
}
}
7. Vamos fazer também a listagem das Conta s com o uso da classe TestaListagemConta , que deve
acessar nosso DAO e fazer:
public class TestaListagemConta {
public static void main(String[] args) {
EntityManager manager = // recupera um EntityManager
manager.close();
}
}
8. Crie a classe TestaRemoveConta para testar a exclusão de uma Conta pelo ContaDao :
public class TestaRemoveConta {
public static void main(String[] args) {
EntityManager manager = // recupera um EntityManager
manager.getTransaction().begin();
manager.getTransaction().commit();
manager.close();
}
}
9. Execute novamente o teste da listagem das contas e verifique se ela foi realmente excluída.
10. Para finalizarmos o CRUD da Conta vamos fazer a alteração. Para isso crie a classe
TestaAlteraConta com o seguinte conteúdo:
manager.getTransaction().begin();
manager.getTransaction().commit();
manager.close();
}
}
O que fizemos na verdade, foi um mapeamento objeto-relacional via JPA, para recuperar registros
do banco de dados. Esse objeto devolvido pelo método find() tem como garantia que, enquanto
estiver em uma sessão aberta do EntityManager , manterá sincronismo com sua representação no
banco de dados de algum forma. Esse estado do objeto é conhecido na JPA como Managed
(Gerenciado). Nesse estado, o objeto (ou entidade) é mantido no contexto de persistência (ou
Persistence Context) e garante-se que sua referência em memória será automaticamente sincronizada
com sua representação no banco de dados sempre que houver alguma alteração (como uma chamada de
setter por exemplo). Isso se dá no momento da chamada do método commit da transação aberta do
EntityManager .
// Recupera um EntityManager
EntityManager manager = new JPAUtil().getEntityManager();
// Altera o titular
conta.setTitular("Novo titular");
// Consolida as modificações
manager.getTransaction().commit();
manager.close();
}
}
Observe que o find quando executado faz a JPA gerar um SELECT para buscar o registro no
banco, depois, quando o commit é feito, a JPA automaticamente gera um UPDATE para as alterações
feitas no objeto, mantendo assim o seu sincronismo com sua representação no banco de dado.
Hibernate:
select
conta0_.id as id1_0_0_,
conta0_.agencia as agencia2_0_0_,
conta0_.banco as banco3_0_0_,
conta0_.numero as numero4_0_0_,
conta0_.titular as titular5_0_0_
from
Conta conta0_
where
conta0_.id=?
Hibernate:
update
Conta
set
agencia=?,
banco=?,
numero=?,
titular=?
where
id=?
Sendo assim, nosso grande objetivo é tornar nossas entidades Managed para que a JPA possa
gerenciá-las dentro do Persistence Context.
Perceba que logo após o persist , o objeto se torna Managed. Portanto, se for feita qualquer
alteração neste objeto após o persist() , o Hibernate se encarregará de fazer o devido UPDATE .
Conta contaNova = new Conta();
contaNova.setTitular("Marcos");
Transaction t1 = manager.getTransaction();
t1.begin();
manager.persist(contaNova);
t1.commit();
Transaction t2 = manager.getTransaction();
t2.begin();
contaNova.setTitular("Manolo");
t2.commit();
Existem outras formas de se passar para o estado Detached. Simplesmente fechando ( close() ) ou
limpando ( clear() ) a sessão do EntityManager , todos os objetos que estiverem no Persistence
Context passaram de Managed para Detached imediatamente, ou se chamarmos explicitamente o
método detach do EntityManager passando um objeto que deva ser desanexado. Só não podemos
esquecer que um objeto Detached não será mais sicronizado com o banco de dados.
EntityManager manager = // recupera um EntityManager
// Fechando o EntityManager
// Conta2 e Conta3 também passam para estado detached
manager.close();
Quando queremos atualizar as informações do banco de dados com um objeto no estado Detached
ou Transient com id preenchido, utilizamos o método merge do EntityManager . Esse método cria
uma entidade no estado managed, copiará as informações da entidade detached que foi passada como
argumento e por fim devolve a entidade managed como retorno do método. Note que o argumento do
método merge não muda para o estado managed, portanto se fizermos qualquer modificação no objeto
que depois da chamada do merge, essas modificações não serão persistidas no banco de dados.
manager.getTransaction().begin();
manager.getTransaction().commit();
manager.close();
manager.getTransaction().begin();
manager.remove(conta);
manager.close();
Podemos reverter qualquer remoção de registros no banco de dados feitos pela JPA simplesmente
invocando o método persist para um objeto no estado Removed. Imediatamente ele passa para o
estado Managed e é sincronizado com o banco.
Uma forma de forçar que todas as entidades Managed no Persistence Context sejam
sincronizadas com o banco de dados imediatamente, antes mesmo do commit da transação, é
chamar o método flush do EntityManager .
Muito mais interessante do que considerar o Hibernate, ou qualquer outra implementação da JPA2,
apenas como um gerador de SQL, é olhar para ele como um gerenciador do estado persistente dos
nossos objetos. Portanto, usamos o Hibernate para fazer com que os nossos objetos estejam no estado
Managed, já que assim terão a representação no banco.
É importante conhecer esses estados e o ciclo de vida de uma entidade em relação ao seu
EntityManager , pois lhe poupará de problemas simples que podem comprometer a aplicação.
O diagrama abaixo mostra exatamente como transitar entre os possíveis estados das entidades na
JPA.
CAPÍTULO 5
// getters e setters
}
Utilizaremos BigDecimal para não perder informações de um número com ponto flutuante, que é
bastante importante num caso como esse, onde guardamos valores financeiros. Mais informações e
detalhes sobre o uso dessa classe versus double :
http://blog.caelum.com.br/arredondamento-no-java-do-double-ao-bigdecimal/
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Para contornar esse problema, teríamos que fazer validações nos nossos métodos e gastaríamos
tempo codificando detalhes que não fazem parte do problema que deveria ser resolvido e que na verdade
são problemas de infra estrutura, algo que já vimos que não queremos cuidar e quem deve ser
responsável por isso é o framework ORM, no caso a implementação da JPA que estamos utilizando.
No Java 5, foi introduzida uma nova forma de declarar tipos que nos permite criar objetos que
representem constantes no nosso sistema, a Enum . Para representar o tipo de movimentação, teríamos:
public enum TipoMovimentacao {
ENTRADA, SAIDA;
}
Precisamos configurar a Movimentacao para ela ser gerenciada pela JPA. A classe com as anotações
Um detalhe aqui: temos um atributo que é uma Enum e este não tem uma representação direta no
banco de dados. Por exemplo, se temos um atributo do tipo String , geralmente na tabela, ele viraria,
no caso do MySQL, uma coluna do tipo varchar. Para a Enum , no entanto, não temos uma
representação direta. Temos que anotar nosso atributo com uma anotação diferente, para ensinar à JPA
como tratá-lo. Existe uma anotação chamada @Enumerated , e é essa que vamos usar para anotar o tipo
da movimentação.
A JPA define duas abordagens para mapear atributos do tipo Enum . Podemos falar para ela criar um
campo de um tipo numérico na tabela e então ela mapearia cada constante criada a partir da Enum
como um número na tabela.
Para isso, ele segue a ordem das constantes da nossa Enum . Por exemplo, para o
TipoMovimentacao , a ENTRADA seria salva como 0 e a SAIDA como 1.O problema dessa abordagem é
que, caso criássemos mais uma constante, e esta fosse colocada no inicio da nossa Enum , ela agora
passaria a ser salva como 0 e todo nosso código legado seria quebrado. Para conseguirmos isso basta
anotar o atributo tipo com a anotação @Enumerated .
Para exemplificar o mapeamento usando este estilo, nosso atributo ficaria da seguinte forma:
@Enumerated(EnumType.ORDINAL)
private TipoMovimentacao tipoMovimentacao;
A outra abordagem existente é informar à JPA que você quer mapear a Enum como uma String .
Com isso, no banco, cada constante seria representada pelo seu nome. A ENTRADA por exemplo, seria
salva com o texto ENTRADA. Dessa forma não precisamos nos preocupar com criações de novas
constantes, com certeza ela terá um nome diferente e não vai impactar no código legado. Para isso,
vamos usar a mesma anotação Enumerated mas vamos informar que é para ser gravado no banco
como texto. A nossa classe com essa alteração fica da seguinte forma:
@Entity
public class Movimentacao {
@Id
@GeneratedValue(GenerationType.IDENTITY)
private Integer id;
private String descricao;
private LocalDateTime data;
@Enumerated(EnumType.STRING)
private TipoMovimentacao tipoMovimentacao;
}
Por default, a JPA vai usar a EnumType.ORDINAL , portanto vamos sobrescrever o comportamento
padrão e especificar que queremos usar o EnumType.STRING para evitarmos problemas futuros com a
ordem dos elementos da Enum .
O problema aqui é que a JPA 2.1 foi lançada antes do Java 8 e não possui um suporte nativo para as
classes da nova API TIME, ou seja, pela especificação não é possível armazenar diretamente um
LocalDateTime em uma coluna do tipo TimeStamp. Para resolver esse problema seguindo a
especificação, precisaríamos converter o nosso atributo para java.sql.Timestamp , como explicado no
box abaixo.
Felizmente, nossa implementação da JPA 2.1, o Hibernate 5.6.2, já possui esse conversor que é
necessário, portanto nada precisamos fazer ao mapear nossa classe Movimentacao como uma entidade
da JPA.
Caso utilizemos uma implementação da JPA 2.1 que não possua um conversor assim como o
Hibernate 5, o nosso atributo data do tipo LocalDateTime será armazenado no banco de dados
em uma coluna do tipo BLOB ou Binary Large Object , ao invés de fazê-lo em uma coluna do
tipo TimeStamp .
Para que os dados sejam armazenados da forma desejada, precisamos construir um conversor
que transforme objetos do tipo LocalDateTime em java.sql.TimeStamp antes de serem
persistidos, conforme mencionado anteriormente. Então, basta criarmos uma classe que
implemente a interface AttributeConverter definida pela JPA 2.1. Teríamos algo assim:
@Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timest
amp> {
@Override
public Timestamp convertToDatabaseColumn(LocalDateTime data) {
if(data == null){
return null;
}
return Timestamp.valueOf(data);
}
@Override
public LocalDateTime convertToEntityAttribute(Timestamp data) {
if(data == null){
return null;
}
return data.toLocalDateTime();
}
}
Observe que a interface já define dois métodos para convertermos nossa entidade para o tipo da
coluna do banco de dados e vice-versa. Por fim, anotamos a nossa classe com @Converter para
indicarmos à JPA que temos um conversor de atributos. A propriedade autoApply define que o
conversor sempre será utilizado para atributos desse tipo, no nosso caso, o LocalDateTime .
@Entity
public class Movimentacao {
@Temporal(TemporalType.TIMESTAMP)
private Calendar data;
No entanto, nossa entidade Movimentacao possui um atributo Conta . Como a JPA mapeará esse
atributo do tipo Conta no banco de dados? Um varchar , um decimal , um int ? Como ela não
conhece por padrão esse tipo de dados, nós precisamos mapeá-lo.
O que queremos mapear é um relacionamento entre ambas as entidades, que no banco de dados
deverá ser refletido por uma chave estrangeira. A única informação que precisamos dizer é qual a
cardinalidade da Movimentacao com relação a Conta . Como, nesse caso eu posso ter na minha
aplicação diversas movimentações, mas cada uma só possui uma única conta, a cardinalidade desse
relacionando é muitos para um. Essa cardinalidade nós representamos no nosso código Java com a
anotação @ManyToOne , como a seguir:
@Entity
public class Movimentacao {
@Id
@GeneratedValue(GenerationType.IDENTITY)
private Integer id;
private String descricao;
private LocalDateTime data;
@ManyToOne
private Conta conta;
@Enumerated(EnumType.STRING)
private TipoMovimentacao tipoMovimentacao;
}
Com isso, a implementação da JPA esperará que exista uma tabela chamada Movimentacao e uma
das suas colunas se chamará conta_id que é uma chave estrangeira apontando para o id da tabela
Conta .
Para aqueles que precisam do máximo de customização, você ainda pode definir atributos sobre a
coluna de chave estrangeira, por exemplo:
@ManyToOne
@JoinColumn(name="conta_pk")
private Conta conta;
// getters e setters
}
Agora, devemos adicionar os mapeamentos básicos da JPA para a nossa entidade. Para isso, vamos
adicionar a anotação @Entity na nossa classe e as anotações @Id e
@GeneratedValue(strategy=GenerationType.IDENTITY) no atributo id .
2. Vamos agora anotar nosso atributo conta com a anotação @ManyToOne para indicar ao Hibernate
que ele representa o relacionamento no banco de dados.
@ManyToOne
private Conta conta;
3. Crie uma nova classe de teste, cujo método main adicionará uma nova Movimentacao sem
nenhuma Conta . Verifique se a tabela foi criada corretamente, com a chave estrangeira. Caso
necessário, execute no console do mysql :
desc Movimentacao;
Precisamos agora persistir a Movimentacao . Para isso, poderíamos buscar uma EntityManager
através da JPAUtil e persistir a Movimentacao :
EntityManager manager = new JPAUtil().getEntityManager();
manager.getTransaction().begin();
manager.persist(movimentacao);
manager.getTransaction().commit();
manager.close();
O problema é que, até o momento, essa Movimentacao não possui nenhuma Conta associada a
ela. Para resolver esse problema, poderíamos criar uma Conta nova e associá-la a essa movimentação,
como a seguir:
EntityManager manager = new JPAUtil().getEntityManager();
manager.getTransaction().begin();
manager.persist(movimentacao);
manager.getTransaction().commit();
manager.close();
No entanto, ao executarmos esse código em um método main , ele não vai funcionar. O que estará
errado com nosso código? É um problema comum de aparecer, e conhecendo bem o ciclo de vida e
estado das entidades na JPA fica fácil reconhecer esses erros. Vamos testar.
52 5.6 EXERCÍCIOS: TENTANDO CRIAR UMA NOVA MOVIMENTAÇÃO RELACIONADA COM UMA CONTA
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
1. Vamos adicionar uma nova Movimentacao associada a uma Conta . Para isso, crie a classe
TestaSalvaMovimentacaoComConta no pacote br.com.caelum.financas.teste :
manager.persist(movimentacao);
manager.getTransaction().commit();
manager.close();
}
}
2. Execute essa classe e repare que não vai funcionar. Leia a mensagem de erro com atenção. A seguir
vamos entender o problema que está acontecendo e como resolvê-lo.
manager.persist(conta);
manager.getTransaction().commit();
manager.close();
http://blog.caelum.com.br/transientobjectexception-lazyinitializationexception-e-outras-famosas-
do-hibernate/
O problema dessa abordagem é que o join é necessário para ligar as duas tabelas, já que ambas não
se conhecem. O que é diferente no modelo de objetos, pois a Movimentacao sabe que ela possui uma
Conta . Nesse caso, bastaria pedirmos para a JPA a Movimentacao de id 20 e em seguida, na
movimentação retornada invocarmos o método getConta , como a seguir:
System.out.println(movimentacao.getConta().getTitular());
Com apenas duas linhas de código conseguimos o que procurávamos. Ao executar essa query, o
Hibernate vai gerar um SQL parecido com o seguinte:
select
movimentac0_.id as id1_1_,
movimentac0_.conta_id as conta6_1_1_,
movimentac0_.data as data1_1_,
movimentac0_.descricao as descricao1_1_,
movimentac0_.tipo as tipo1_1_,
movimentac0_.valor as valor1_1_,
conta1_.id as id0_0_,
conta1_.agencia as agencia0_0_,
conta1_.banco as banco0_0_,
conta1_.numero as numero0_0_,
conta1_.titular as titular0_0_
from
Movimentacao movimentac0_
left outer join
Conta conta1_
on movimentac0_.conta_id=conta1_.id
where
movimentac0_.id=?
movimentacao.getConta().setTitular("Maria Cecilia");
manager.getTransaction().commit();
manager.close();
O DAO de Movimentacao terá os métodos com as operações básicas que podemos realizar sobre a
entidade e ficará parecido com:
Nesse caso é comum encontrarmos código que excluem todas as movimentações para que seja
possível excluir a Conta . Nesse caso uma abordagem que pode ser seguida é a remoção em cascata,
onde todos os objetos que ficariam "órfãos" também são excluídos.
Com a JPA podemos fazer isso com o uso do atributo cascade nas anotações de cardinalidade do
relacionamento.
Apesar de ser um recurso interessante, abusar dos efeitos de cascata pode tornar o código difícil de
prever: muitos efeitos colaterais podem ocorrer ao invocarmos um simples persist e precisaremos
sempre estudar o mapeamento para saber o que pode acontecer.
Por exemplo, podemos automaticamente preencher a data de alteração quando a movimentação for
salva ou atualizada no banco:
@Entity
public class Movimentacao {
//outros atributos
@PrePersist
@PreUpdate
public void preAltera() {
System.out.println("Atualizando a data da movimentacao");
this.setData(LocalDateTime.now());
}
O método anotado com @PrePersist e @PreUpdate também se chama callback. Os callback_s são
chamados automaticamente quando há alguma mudança no ciclo da vido do objeto e um exemplo de
_inversão de controle que veremos mais para frente.
5.14 ENTITYLISTENER
Para deixar as entidades livres dos métodos de callback (e para não repetir código
denecessariamente), existe a possibilidade de definir os callbacks em uma classe separada. A classe
funciona como um Listener e deve ser declarada através da anotação @EntityListener na entidade.
Em um Listener poderíamos gravar um log, enviar um email, fazer auditoria ou avisar outros
componentes (mandar uma mensagem, por exemplo). Pode ser um grande substituto para os triggers
comumente encontrados nos grandes sistemas, tornando essa regra de negócio independente de banco
de dados.
58 5.14 ENTITYLISTENER
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
@PrePersist
public void prePersist(Object entidade) {
System.out.println("Avisa o financeiro que existe uma nova movimentacao ");
}
}
//atributos e métodos
}
Os listeners podem ser usados para várias entidades diferentes. Repare que um Listener recebe um
Object.
2. Adicione uma nova movimentação no banco (mas sem data) e verifique se o callback for chamado.
CAPÍTULO 6
INTRODUÇÃO A JAVA EE
"Para obtermos êxito no mundo temos de parecer idiotas mas sermos espertos." -- Baron de Montesquieu
Nesse capítulo, você entenderá o porquê do Java EE existir e como instalar o servidor de aplicação
WildFly 10.
//usa o manager
manager.getTransaction().commit();
manager.close();
Repare que somos responsáveis por abrir e fechar todos os recursos, além de cuidar das transações.
Abrimos um EntityManager através do JPAUtil , que por sua vez cria uma
EntityManagerFactory . Depois começamos uma transação, pedindo a transação ao EntityManager .
Por fim comitamos a transação e fechamos o EntityManager .
Isso é um exemplo típico que mostra a ausência de inversão de controle. O nosso código precisa
controlar o ciclo de vida dos objetos indicados pelo uso de factories ( JPAUtil ) e chamadas do
begin() , commit() ou close() . Pior, essas chamadas normalmente se espalham pelo nosso código
dificultando a manutenção e legibilidade.
A coisa ainda se torna mais difícil quando queremos utilizar além da JPA outros componentes. Por
exemplo, um pool de conexões, uma biblioteca de segurança ou um log. Todas essas preocupações caem
sobre o desenvolvedor. Não há nenhuma infraestrutura pronta para utilizar e logo estamos gastando
mais tempo desenvolvendo funcionalidades que não são do domínio como por exemplo:
Autenticação
Autorização
Log e Auditoria
60 6 INTRODUÇÃO A JAVA EE
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
Neste caso, dizemos que é a regra de negócio que está controlando a infraestrutura.
Esse cenário pode ficar ainda mais desanimador se você imaginar que uma aplicação tem muito mais
do que meia dúzia de regras de negócio. Sendo assim, as chamadas aos serviços de infra-estrutura
ficariam espalhadas por toda a aplicação, dificultando sua manutenção.
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Nossa aplicação deveria se concentrar nas regras de negócio (ou lógica de negócio, business logic).
Porém, acabamos nos preocupando com toda a infraestrutura da aplicação, e todos os seus detalhes,
como o tamanho do pool de conexões e o gerenciamento do JPA.
Estes serviços de infraestrutura costumam estar espalhados por todas as lógicas de negócios.
Exemplos disso são log e persistência no banco de dados, que não deveriam ser escritos pelo
programador, pois o ideal é que ele foque em regras do negócio.
Seria interessante a nossa lógica de negócios ser chamada por alguém que já nos entregue todos os
serviços de infraestrutura prontos.
Em uma aplicação grande, perderíamos muito tempo escrevendo um pool de conexões realmente
eficiente, por exemplo. E, nesta mesma aplicação, a lógica de negócios já é demasiadamente complexa,
impedindo que você gaste tempo com a infraestrutura e detalhes de performance, clusterização,
segurança, etc.
Estamos invertendo o controle da nossa aplicação. Não somos mais responsáveis pela infraestrutura
da aplicação, e melhor, não somos responsáveis por fazer essas chamadas de serviços comuns: alguém
será responsável por fazer isso.
Java EE nada mais é do que uma série de especificações definidas pelo Java Community Process (JCP
- jcp.org). Cada especificação é definida em um Java Specification Request (JSR), um documento que
descreve detalhadamente como deve funcionar um determinado serviço.
JAVA EE 7
A nova versão do Java EE já saiu com várias atualizações que facilitam ainda mais a vida do
desenvolvedor com foco na produtividade. Além de atualizações de vários especificações como JMS
2.0, JAX-RS 2.0, JSF 2.2 entre outras, também foram introduzidas novas especificações para
processamento em lote e JSON.
blog.caelum.com.br/novidades-javaee7-2/
Existem vários servidores no mercado, a maioria compatível com a versão 1.4 do Java EE. Alguns dos
mais famosos são:
Oracle Glassfish
JBoss Application Server
WildFly
Apache Geronimo
IBM WebSphere
Oracle WebLogic
http://www.oracle.com/technetwork/java/javaee/overview/compatibility-jsp-136984.html
SERVLET CONTAINER
Um servlet container não atende todas as especificações mencionadas. Ele somente implementa
as especificações de JSP e Servlets. O Tomcat (http://tomcat.apache.org) e Jetty são famosos serlvet
containers.
O servidor de aplicação WildFly, por sua vez, usa o Undertow e implementa todas as
tecnologias do Java EE.
No caso do Tomcat há uma versão que além dos servlets implementa outras especificações
como JSF, EJB e JTA. Esse profile se chama Web Profile e o servidor Apache TomEE
(http://tomee.apache.org) o atende.
RI - REFERENCE IMPLEMENTATION
O servidor Glassfish é a base para a implementação de referência (RI) do Java EE. Isso significa
que o Glassfish sempre será o pioneiro na implementação das novas especificações, e que você pode
confiar nele para saber como o servidor deve se comportar em determinados casos.
bin/client - libs necessárias pelo cliente, aqui você encontra os JARs que, por exemplo,
implementam a busca no serviço de nomes JNDI do WildFly e classes que são responsáveis
por gerar os stubs magicamente, através das proxies dinâmicas
docs - documentação
domain - Arquivos referentes ao modo domain (veremos mais a respeito do modo domain
nos apêndices)
O WildFly possui dois modos de operação - modo standalone, que geralmente usamos para
desenvolver as funcionalidades de uma aplicação, e modo domain, que serve, na verdade, para reger um
grupo de servidores. Utilizaremos, por hora, somente o modo standalone.
Caso não possua a opção JBoss Community -> WildFly 10.x , procure por Red Hat JBoss
Middleware -> JBoss AS, Wildfly, & EAP Server Tools :
Clique em Next , aguarde o carregamento dos plugins do JBoss e aceite os termos dos plugins:
Clique em Finish e proceda com a instalação dando OK se aparecer algum box de confirmação da
instalação. Ao final do procedimento, será solicitado que você reinicie o Eclipse.
Faça isso e volte novamente para a aba Servers . Clique com o botão direito e vá em New ->
Server . Agora expanda a opção JBoss Community e escolha WildFly 10.x .
Aperte Next até chegar na tela de escolha do diretório do WildFly. Clique em Browse e selecione a
pasta onde você descompactou o WildFly.
Vamos utilizar as configurações padrão mas com uso de cache no servidor, por isso precisaremos
alterar o arquivo de configuração do servidor (Configuration file) de standalone.xml para
standalone-ha.xml. Faça isso clicando em Browser e buscando o arquivo em
/home/jpaXXXX/wildfly-10.1.0.Final/standalone/configuration/ . Clique em OK e depois
em Finish .
6.9 EM CASA
Em sua casa, faça o download do WildFly no site http://wildfly.org/downloads/.
Escolha a última versão da série 10.1.x Final, que implementa a especificação Java EE 7, incluindo a
especificação JPA 2.1, CDI 1.1 e EJB 3.2.
Você precisa da variável de ambiente JAVA_HOME configurada, para o diretório de instalação do seu
JDK.
Basta, agora, executar o script standalone dentro do diretório bin . Existe um arquivo .sh para
ambientes Unix e .bat caso esteja no Windows.
74 6.9 EM CASA
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
Outro ponto importante: certifique-se de que seu Eclipse aponta para uma JDK e não para uma JRE.
Para isso, faça o download da JDK em
http://www.oracle.com/technetwork/java/javase/downloads/index.html e, em seguida, no Eclipse, vá em
Window -> Preferences -> Java -> Installed JREs -> Add e adicione a JDK que você fizer
download. Isso evitará conflitos e problemas com o plugin do JBoss AS.
6.9 EM CASA 75
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
CAPÍTULO 7
"O primeiro problema para todos, homens e mulheres, não é aprender, mas desaprender" -- Gloria Steinem
Uma aplicação que possui diversos usuários enviando requisições precisa se conectar ao banco de
dados para conseguir realizar consultas e devolver resultados para os clientes. Abrir conexões a todo o
momento é bastante custoso, pois um socket deve ser aberto e gera um tráfego na rede para chegar até o
servidor de banco de dados.
Em um sistema Web, por exemplo, vamos abrir uma nova Connection para cada request feito? E se
estivermos criando um portal com milhares de acessos simultâneos, nosso banco aguentaria milhares de
conexões? Além disso, abrir uma conexão é um processo relativamente custoso para se fazer a cada novo
request.
Será que é melhor então criar apenas uma conexão e compartilhá-la com todos os clientes? E se
alguém faz uma query mais demorada? Todos os outros ficam esperando?
Perceba que não queremos uma conexão só, mas também não queremos uma conexão para cada um.
Queremos um número N de conexões. E, claro, esse número depende da aplicação que estamos
desenvolvendo (número de clientes, quanto o banco aguenta, quantas aplicações usam o mesmo banco,
etc).
A parte da aplicação que gerencia quantas conexões devem ser abertas e as gerencia para os clientes é
chamada de Connection Pool. É boa prática, quando trabalhamos com Bancos de Dados, sempre usar
um Connection Pool, inclusive quando não se usa Hibernate e sim JDBC puro.
O Hibernate já tem suporte nativo com seu próprio pool de conexões. No entanto, esse não é o
recomendado para o uso em produção. Ao iniciar a EntityManagerFactory da sua aplicação, o Hibernate
gera o seguinte log:
Nem sempre isso é desejável ou possível. Há aplicação onde o desenvolvedor não deve conhecer os
dados de autenticação do banco. Também pode fazer sentido compartilhar um único pool com várias
aplicações dentro do mesmo servidor de aplicação.
Por fim, se o servidor oferece um pool de conexões como serviço para a nossa aplicação, podemos
também alterar o tamanho do pool sem "redeployar" a aplicação. Dependendo do servidor, essa alteração
pode ser feita ao vivo, sem causar um desligamento do servidor.
A solução para esses problemas, no mundo Java EE, se chama DataSource . Um datasource
representa uma ligação genérica com uma fonte de dados (normalmente banco do dados), encapsula os
dados de autenticação e usa também um pool de conexões por baixo.
Uma vez feita a configuração do datasource no servidor, podemos nos referenciar a ele no
persistence.xml .
Como o WildFly possui um banco embutido, o H2, nem é preciso configurar essse datasource. Ele já
está pronto e acessível pelo contexto JNDI: java:jboss/datasources/ExampleDS .
Foi declarado o nome JNDI e os dados de acesso. Também podemos colocar configurações de pool
no arquivo standalone-ha.xml , da seguinte forma:
<datasource jndi-name="java:jboss/datasources/ExampleDS"
pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;
DB_CLOSE_ON_EXIT=FALSE</connection-url>
<driver>h2</driver>
<pool>
<min-pool-size>10</min-pool-size>
<max-pool-size>20</max-pool-size>
<prefill>true</prefill>
</pool>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
Como a configuração do banco está separada, o arquivo persistence.xml fica muito mais simples:
<persistence>
<persistence-unit name="loja" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
</persistence-unit>
</persistence>
Repare que usamos a tag jta-data-source para referenciar o datasource no contexto JNDI. O
ExampleDS representa, como já foi mencionado, o nome JNDI do banco H2, ou seja, é apenas um
apelido. No entanto, poderia ser utilizado qualquer outro banco. Só pelo arquivo persistence.xml
não tem como saber qual banco é realmente usado. A configuração está totalmente desacoplada da
aplicação. Veremos mais adiante como configurar um outro datasource para o MySQL.
Repare que isso faz parte do conceito de inversão de controle. A aplicação não se preocupa com
destalhes da configuração do banco. Os dados de acesso e o pool de conexões não fazem parte da
aplicação, pois ela delega esta preocupação para o servidor de aplicação usando um datasource.
A desvantagem dessa abordagem é que sua aplicação fica dependente de uma configuração do
servidor de aplicação e perde a portabilidade. Sempre antes de deployar a aplicação é preciso configurar
o datasource.
TRANSAÇÃO JTA
O tipo de transação deve ser obrigatoriamente JTA , que é o padrão da especificação Java EE.
Dentro de um servidor de aplicação não temos outra escolha. Por enquanto, como não precisamos
nos preocupar com transações, deixaremos o servidor decidir o que fazer com JTA (novamente
inversão de controle).
Como nosso banco será o MySQL, precisaremos definir um datasource pra ele. Antes disso, devemos
colocar o JAR do driver do MySQL no Wildfly. Isso pode ser feito de diversas maneiras, entretanto a
mais recomendada consiste em criar um módulo contendo o recurso.
Para criarmos um módulo no WildFly que contenha o driver do MySQL, precisamos seguir algumas
etapas.
com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
</xa-datasource-class>
</driver>
Agora que criamos um módulo para o JAR do driver do MySQL, vamos efetivamente declarar o
DataSource do banco. No arquivo standalone-ha.xml, dentro da tag <datasources> , adicionamos
uma nova tag de <datasource ...> :
<datasource jndi-name="java:/fj25DS"
pool-name="fj25DS" enabled="true" use-java-context="true">
<connection-url>jdbc:mysql://localhost:3306/fj25</connection-url>
<driver>com.mysql</driver>
<pool>
<min-pool-size>10</min-pool-size>
<max-pool-size>100</max-pool-size>
<prefill>true</prefill>
</pool>
<security>
<user-name>root</user-name>
</security>
</datasource>
Repare que definimos também o tamanho do pool, ou seja a quantidade de conexões que o pool vai
ter no mínimo ( min-pool-size ) e no máximo ( max-pool-size ).
Para definir um datasource que pode participar em uma transação distribuída (JTA) é preciso definir
um xa-datasource . Precisaremos, então, especificar a classe a ser utilizada no driver para estruturar
uma transação distribuída. Na tag <drivers> , onde houver a declaração do módulo contendo o driver
do MySQL, devemos adicionar a declaração da classe para transação distribuída:
<driver name="com.mysql" module="com.mysql">
<xa-datasource-class>
com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
</xa-datasource-class>
</driver>
Como foi discutido anteriormente, o persistence.xml usa apenas o nome JNDI para se
referenciar ao DataSource :
<persistence>
<persistence-unit name="financas" transaction-type="JTA">
<jta-data-source>java:/fj25DS</jta-data-source>
</persistence-unit>
</persistence>
mkdir -p mysql/main
Dentro do diretório main, copie o arquivo module.xml e o JAR do driver do MySQL que estarão
dentro da pasta Caelum/25 presente no seu Desktop.
<datasource jndi-name="java:/fj25DS"
pool-name="fj25DS" enabled="true" use-java-context="true">
<connection-url>jdbc:mysql://localhost:3306/fj25</connection-url>
<driver>com.mysql</driver>
<pool>
<min-pool-size>10</min-pool-size>
<max-pool-size>100</max-pool-size>
<prefill>true</prefill>
</pool>
<security>
<user-name>root</user-name>
</security>
</datasource>
Reinicie o Wildfly, e, se tudo correr bem, você deve encontrar uma linha similar a essa no seu log:
O Hibernate possui integração com vários pools de conexão reconhecidos do mercado, entre eles o
apache dbcp pool, proxool e o C3P0. Nós vamos usar o C3P0 que é bastante indicado nos livros pela
própria equipe do Hibernate.
Para configurar o C3P0, além do JAR no classpath do seu projeto, você precisa colocar as seguintes
linhas no persistence.xml :
Só pelo fato de termos alguma propriedade com hibernate.c3p0. , o Hibernate já ativa o C3P0
como implementação de pool. A configuração min_size indica o número mínimo de conexões que o
pool deixa preparada a qualquer momento. A max_size indica o número máximo de conexões no pool.
Esses valores têm que ser determinados com base em diversos fatores da sua aplicação (carga de
usuários, quanto seu banco aguenta e etc).
O timeout indica depois de quanto tempo uma conexão inativa (idle) deve ser retirada do pool. O
C3P0 usa um sistema inteligente em que threads testam se as conexões estão vivas de tanto em tanto
tempo, configurando o idle_test_period .
Existe a possibilidade de você ligar o test_on_burrow de alguns connection pools, fazendo com que
o Hibernate teste se a conexão está viva toda vez que uma conexão for pega do pool. O C3P0 diz que isso
é extremamente mais lento que testar de tempos em tempos com idle_test_period , porém evita ao
máximo que uma conexão quebrada seja utilizada. Mais informações:
http://www.hibernate.org/214.html
2. Vamos testar a abertura de conexão ilimitada pelo Hibernate. Crie uma classe
TestaAberturaConexoes e gere o método main .
Antes de fechar o método main , mas depois do loop, deixe a Thread dormir. Não esqueça de deixar
o Eclipse tratar a Exception com throws InterruptedException :
// Para que o thread durma por 30s
Thread.sleep(30 * 1000);
Depois verifique as conexões abertas no MySQL (dentro do intervalo de 30s), para isso conecte-se
com o banco de dados e visualize as conexões abertas:
4. Copie os JARs do C3P0 que estão na pasta do Desktop Caelum/25/c3p0 para a pasta lib do seu
projeto ( ~/workspace/fj25-financas/lib ). Não esqueça de dar um Refresh no projeto, quando
voltar ao Eclipse, e de adicionar os novos JARs ao Build Path da aplicação (botão direito nos
JARs , Build Path -> Add to Build Path )
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
CAPÍTULO 8
O projeto web utilizará o JSF 2 e já possuirá telas iniciais para que precisemos apenas trabalhar com
os DAOs, entidades e queries, de tal maneira que não seja necessário conhecimento das tags do JSF nem
dos Managed Bean s. O JSF 2 em si é profundamente abordado no curso FJ-26, juntamente com CDI.
O projeto web que utilizaremos será composto por telas onde é possível fazer o CRUD de Conta e
Movimentacao . Além disso, possui uma tela com um menu principal onde temos disponível algumas
opções de relatórios que vamos desenvolver conforme a evolução no treinamento.
2. Agora vamos importar um projeto previamente criado, contendo, além das classes que já foram
escritas durante o curso, as classes e páginas relativas ao JSF para que possamos visualizar os
resultados das nossas consultas numa interface web. Para isso siga os seguintes passos:
Clique com o botão direito em cima do nome do projeto, no caso fj25-financas-web e escolha a
opção: Import
Clique em Finish.
Para isso, vá até a aba Servers e clique com o botão direito sobre o WildFly . Em seguida
escolha a opção Add and Remove... :
5. Navegue pelas telas para se acostumar com a aplicação. Note que muitos recursos ainda não vão
funcionar, pois não estamos usando os DAOs e nem o banco de dados que configuramos nos
passos anteriores.
No entanto, a JPA ainda não sabe onde se encontra o datasource. Para isso, é necessário configurar o
endereço dele no persistence.xml . Repare que usamos a tag jta-data-source abaixo para
referenciar o datasource no contexto JNDI:
<persistence>
<persistence-unit name="financas" transaction-type="JTA">
<jta-data-source>java:/fj25DS</jta-data-source>
</persistence-unit>
</persistence>
responsável pela criação de EntityManager s. Como usamos um datasource, seria preciso buscar a
EntityManagerFactory no JNDI antes de subir a JPA. Dentro de um servidor de aplicação isso não é
necessário.
CONTEXTO JNDI
Em geral, cada serviço registrado no JNDI está disponível para as aplicações dentro do WildFly.
Podemos enxergar o JNDI como um registro para cadastrar qualquer objeto. Esse registro é
organizado em forma de árvore onde os nós tem um objeto associado.
É possível cadastrar e acessar o contexto JNDI por uma API, mas normalmente isso não é
preciso e o EJB container fará esse trabalho seguindo o modelo da inversão de controle.
Usar JPA dentro do servidor é muito simples uma vez que temos um DataSource configurado.
Basta adicionar no arquivo persistence.xml a tag vista na seção anterior e criar uma classe que recebe
o EntityManager . Essa classe será o nosso DAO - nela injetaremos o EntityManager pela anotação
@PersistenceContext .
@PersistenceContext
private EntityManager manager;
Os Entity Beans são as nossas entidades, por exemplo a classe Conta anotada com @Entity . Os
Message Driven Beans são relacionados com a especificação JMS, receptores de mensagens, vistos em
detalhes no treinamento FJ-36.
No nosso caso, precisamos dos Session Beans - o EJB mais utilizado do mercado. Um Session Bean
nada mais é do que um objeto administrado pelo EJB Container e que ganha vários serviços, como
transação ou injeção de recursos (como já discutimos).
A configuração do Session Bean é muito simples e sem nenhuma burocracia. Basta usar a anotação
@Stateless em cima da classe para transformar a classe ContaDao em um Session Bean Stateless.
@Stateless
public class ContaDao {
@PersistenceContext
private EntityManager manager;
Com essa simples configuração, o EJB Container vai cuidar de todo o ciclo de vida do Session Bean e
injetará o EntityManager quando for necessário.
Vimos o tipo mais importante dos EJBs: o Session Bean Stateless (SBSL). Além dele, há mais 2
outros tipos de Session Beans: Stateful e Singleton.
Estamos vendo apenas uma parte do que EJBs são capazes e oferecem para o desenvolvedor. Há
vários outros serviços que um Enterprise Java Beans recebe de graça. Segue uma lista dos serviços
mais importantes:
No treinamento FJ-36 é visto em detalhe os tópicos de integração entre sistemas com JMS e
Web Services (SOAP e REST).
Quando trabalhamos com JPA sem servidor de aplicação, somos responsáveis pela abertura (begin),
consolidação (commit) e cancelamento (rollback) de transações. Existe o objeto EntityTransaction da
JPA, que permite a demarcação programática de transações - início e fim. Porém, dentro do servidor de
aplicação isso não é possível:
Como a transação é do tipo JTA, não podemos utilizar uma transação gerenciada pelo JPA. O EJB
Container vai automaticamente abrir e fechar a transação sem o desenvolvedor se preocupar com isso,
novamente aplicando o conceito de inversão de controle. Para salvar uma conta dentro de um Session
Bean não precisamos alterar em nada o código:
@Stateless
public class ContaDao {
@PersistenceContext
private EntityManager manager;
over Configuration.
O EJB Container fica com a responsibilidade de iniciar, consolidar (fazer commit) ou desfazer a
transação (rollback). O programador não pode, e nem deve, usar os métodos de controle transacional
nessa estratégia. Essa estratégia é conhecida como Container Managed Transaction - CMT.
No modo CMT, o padrão é executar todos os métodos do Session Bean dentro de transação. O EJB
Container faz uma verificação toda vez que um método de negócio é invocado para saber se uma
transação já está aberta ou não. Se não houver transação aberta, o EJB Container abre uma nova antes de
começar a processar o método. A transação é fechada no final do método em que ela foi aberta. Em
outras palavras, se já existe uma transação aberta use-a, caso contrário, inicie uma nova. Essa estratégia é
conhecida como REQUIRED .
As páginas do sistema são definidas por arquivos .xhtml que declaram os componentes JSF. Segue
um pequeno trecho do arquivo contas.xhtml :
<h:form>
<h:outputText value="Titular"/>
<h:inputText value="#{contasBean.conta.titular}"/>
Nele definimos um formulário, onde ficam os componentes de input (nesse exemplo o titular da
conta) e, no final, há um botão para confirmar o cadastro da conta. Repare que usamos a expression
language para associar o componente input ou botão com uma classe Java, por exemplo no atributo
action: #{contasBean.grava} .
A expressão procura uma classe ContasBean , que já está preparada, dentro do pacote
br.com.caelum.financas.mb . Dentro do pacote já há várias outras classess. São essas classes que
recebem os dados da a interface gráfica, devolvendo-os quando necesário. Nas classes encontram-se os
métodos que serão chamados quando apertarmos um botão. Por exemplo, encontraremos o método
grava na classe ContaBean com o seguinte código:
@Named
@ViewScoped
public class ContasBean {
Como podemos observar, o código acima não faz nada. Apenas imprime uma mensagem no console
indicando que a operação foi feita. Precisamos alterar esses dados para utilizar os DAOs, lembrando que
os DAOs são objetos administrados pelo container EJB. Queremos usar o Session Bean em uma classe
relacionada a JSF. O problema é que precisamos integrar o container EJB com o container JSF.
Para tal, o Java EE oferece uma solução elegante e flexível através da especificação CDI (Context e
Dependency Injection). CDI preocupa-se puramente com o gerenciamento de objetos e a injeção de
dependências, até bem parecido com o EJB. A diferença é que o CDI, por ser mais leve, não implementa
por padrão serviços como transação ou segurança, deixando isso para o EJB Container.
No código acima da classe ContasBean já usamos um pouco do CDI. Com a anotação @Named ,
podemos referenciar a classe com a Expression Language no arquivo xhtml . A anotação @ViewScoped
informa ao CDI que um objeto do tipo ContasBean deve ser instanciado e preservado enquanto a tela
(view) estiver ativa. Isso é feito guardando temporariamente o objeto na sessão do usuário e
automaticamente removendo-o quando o usuário sair da tela atual:
@Named
@ViewScoped
public class ContasBean {
// demais códigos omitidos
}
@Inject
private ContaDao contaDao;
// use o contaDao
}
O único passo que nos falta é invocar o método adiciona do ContaDao . Para isso, precisamos
passar para o método uma Conta , que será populada pelo JSF com os dados que virão da tela. Para isso,
basta termos o atributo Conta na classe ContaBean e seu respectivo getter.
@Named
@ViewScoped
public class ContasBean {
@Inject
private ContaDao contaDao;
1. O primeiro passo é transformar os DAOs que escrevemos anteriormente em EJB Session Bean para
que o EntityManager e a transação sejam gerenciados pelo container.
@Stateless
public class ContaDao {
// código omitido
}
O arquivo persistence.xml já vem pré-configurado para ser usado com nosso datasource. Abra
uma vez o arquivo e verifique a configuração pelo elemento jta-data-source .
2. A classe ContasBean do JSF é usada por trás da tela de adição e listagem de novas Conta s. Por
enquanto, está em branco.
Use o ContaDao que preparamos para adicionar o atributo conta no banco de dados. Isso deve
ser feito no método grava() da classe ContasBean , que está no pacote
br.com.caelum.financas.mb , que deve, ainda, atualizar a lista de contas após a adição.
Mas, para o método grava() funcionar, é preciso pedir ao CDI para injetar o ContaDao que
será usado no ContasBean . Declare então um atributo de instância contaDao e anote-o com
@Inject :
@Named
@ViewScoped
public class ContasBean {
@Inject
private ContaDao contaDao;
Acesse o link Cadastro de contas e cadastre novas contas. Repare no console do servidor se o SQL
de INSERT foi impresso.
E se você tentar alterar uma conta? O que acontece? Em breve vamos resolver essa questão.
3. Agora que conseguimos gravar novas contas, vamos fazer com que a listagem das contas já
cadastradas apareça, atualizada, logo que entramos na página. Para isso, vamos alterar o método
getContas da ContasBean . Use o ContaDao para obter a listagem de contas e grave esse
resultado no atributo this.contas da classe.
É importante verificar se o atributo já não foi preenchido, para evitar executar uma nova query
desnecessariamente. Um esboço da implementação:
public List<Conta> getContas() {
if (this.contas == null) {
this.contas = contaDao.lista();
}
return this.contas;
}
4. Permitiremos a exclusão das contas. Use novamente o ContaDao para a remoção através do método
remove . Um detalhe importante é que o JSF nos dará apenas o ID da conta a ser removida. Antes
de remover, precisamos recuperar o objeto completo através do find() no método remover do
DAO. E, após a remoção, precisamos atualizar a lista de contas.
limpaFormularioDoJSF();
}
movimentacaoDao.adiciona(movimentacao);
}
Por fim, precisamos alterar o método grava() da classe ContasBean para fazer a alteração da
Conta caso o ID seja informado. Para isso, vamos fazer um if antes de adicionar a conta no
DAO:
if (this.conta.getId() == null) {
contaDao.adiciona(conta);
} else {
contaDao.altera(conta);
}
CAPÍTULO 9
Como a JPA já vem bem integrada com os Session Beans, usamos a anotação
@PersistenceContext para receber um EntityManager :
@Stateless
public class ContaDao {
@PersistenceContext
private EntityManager manager;
Uma pergunta que poderia surgir é: Em que exato momento acontece a injeção do
EntityManager ?
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Ou seja, para responder a pergunta inicial, o EntityManager é injetado após da criação do Session
Bean (item 2).
O método de pós-construção é opcional, podemos usar quando acharmos necessário. Basta colocar
um método anotado com @PostConstruct :
@Stateless
public class ContaDao {
@PersistenceContext
private EntityManager manager;
@PostConstruct
void aposInjecao() {
//EntityManager já está disponível
System.out.println("método chamado pelo container na criação");
}
@PreDestroy
void preDestroi() {
System.out.println("método chamdo antes o bean morrer")
}
Se um cliente está na mesma JVM do Session Bean ele é denominado cliente local. Eis uma lista
de possíveis tipos de clientes locais:
Cada vez que o cliente chama um método, o container seleciona alguma instância do pool para
atender ao chamado. No final do processamento, a instância é devolvida ao pool para aguardar mais
clientes.
Se o número de chamadas diminui, as instâncias que estão no pool ficam mais tempo ociosas. Ao
perceber essa situação, o container pode eliminar algumas instâncias para liberar memória. Quando uma
instância é eliminada, o container invoca o método de callback anotado com @PreDestroy .
Portanto, você precisa ter muito critério para colocar atributos de instância em um Session Bean
Stateless. Sempre tenha em mente que esses atributos podem ser compartilhados (não simultaneamente)
por mais de um cliente. Por exemplo, um atributo que representa uma configuração não tem problema
ser compartilhado. Já um atributo que guarda o carrinho de compras de um usuário pode gerar um
problema grave.
Apesar do Session Bean Stateless ser um objeto compartilhado, ao mesmo tempo apenas um cliente
pode fazer a chamada. O acesso ao bean é sincronizado (objetos são threadsafe). Isso significa que, se
temos um Pool com máximo de 5 instâncias, apenas 5 clientes podem ser atendidos ao mesmo tempo. Se
todos os clientes demoram na liberação, outros clientes ficam esperando. Por isso, o Pool pode oferecer
um timeout de acesso para configurar tempo limite de uso.
<pools>
<bean-instance-pools>
<strict-max-pool name="slsb-strict-max-pool" max-pool-size="5"
instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
</bean-instance-pools>
</pools>
Damos um nome ao nosso pool (no caso, o WildFly já vem com esse pool de Beans por padrão e o
nomeia como slsb-strict-max-pool) e em seguida configuramos seus parâmetros, tais como max-pool-
size. Nesse exemplo configuramos o pool com máximo de 5 objetos. Também definimos um timeout de
5 minutos ( instance-acquisition-timeout , instance-acquisition-timeout-unit ), ou seja
nenhuma chamada pode demorar mais de 5 minutos.
A seguir, anotamos a classe do EJB com a anotação específica do WildFly @Pool e informamos qual
pool previamente configurado no arquivo standalone-ha.xml deve ser utilizado (passamos o nome
do pool como parâmetro da anotação). Para que possamos usá-la, o JAR jboss-ejb3-ext-api-
2.0.0.jar deve ser adicionado ao classpath de sua aplicação.
@Stateless
@Pool("slsb-strict-max-pool")
public class ContaDao{
No entanto há situações onde o uso de um pool parece desnecessário. Por exemplo, imagine que
queremos oferecer algumas configurações para toda a aplicação, algo como:
@Stateless
public class ConfiguracoesDaAplicacao{
Para este tipo de Session Bean não faz sentido usar um pool de objetos, é suficiente ter apenas uma
instância em memória. Nas versões anteriores do EJB 3.1 era preciso configurar o pool para garantir
apenas uma instância. Como vimos, a configuração é totalmente específica de servidor e perdemos assim
a portabilidade da aplicação.
Para resolver isso foram criados o Singleton Session Bean, que possuem as mesmas características
do Session Bean Stateless mas garantem uma única instância por aplicação.
@Stateful
public class Carrinho{
Apesar do Session Bean Stateful estar presente desde da primeira versão de EJB, eles nunca fizeram
muito sucesso. Normalmente usa-se a sessão HTTP para guardar dados do cliente, ou, caso queiramos
uma solução persistente, os dados são guardados no banco de dados.
SESSION E STATELESS?
A palavra session passa a ideia de que o estado da conversação com o cliente é mantido. A
palavra stateless passa a ideia oposta. Não se confuda! Stateless Session Bean não mantém estado
conversacional.
Algumas características indicam que seu bean deveria ser Stateful, como possuir atributos
relacionados a um cliente/usuário e possuir mais de um método.
E através da anotação @Local será definida a interface do EJB. Local significa que queremos usar o
Session Bean apenas dentro do mesmo servidor:
@Stateless
@Local(ContaDao.class)
public class ContaDaoBean implements ContaDao {
//...
É possível liberar os Session Bean e usá-los remotamente através de outras aplicações Java. Nesse
caso faremos uma chamada remota e por isso devemos declarar a interface como remoto:
@Stateless
@Remote(ContaDao.class)
public class ContaDaoBean implements ContaDao {
//...
Dessa maneira até podemos liberar o EJB Session Bean para ser usado através Web Services (SOAP).
Isso é de um jeito fácil de configurar, basta usar a anotação @WebService :
@WebService
@Stateless
@Remote(ContaDao.class)
public class ContaDaoBean implements ContaDao {
//...
É importante mencionar que a remotabilidade não faz parte do EJB lite. EJB lite está sendo usado
quando instalamos um servidor no Java EE Web Profile. Para realmente usufruir da remotabilidade
devemos usar um servidor de aplicação completo (Full Profile).
No site da Oracle há uma lista dos servidores e a qual profile eles pertencem:
http://www.oracle.com/technetwork/java/javaee/overview/compatibility-jsp-136984.html
Através desse nome seria possível procurar um Session Bean dentro do contexto do JNDI.
Normalmente isto não é preciso e deve ser evitado, pois a injeção de dependência já faz este papel.
@Stateless
public class Agendador {
@PostConstruct
void posConstrucao() {
System.out.println("criando agendador");
totalCriado++;
}
@PreDestroy
void preDestruicao() {
System.out.println("destruindo agendador");
}
Verifique o método executa . Repare que ele faz nada mais do que simular uma execução
demorada. Além disso, imprimimos a quantidade de instâncias criadas em cada chamada.
3. Abra a classe CicloDeVidaBean . Faça com que ela receba injetado a classe Agendador e em seu
método executeAgendador() chame o método executa() :
public class CicloDeVidaBean {
@Inject
private Agendador agendador;
4. Observação: Todas as configurações abaixo são específicas do WildFly e não portável entre servidores
de aplicações.
Vamos configurar o pool de instâncias para ter, no máximo, cinco instâncias. Para que haja no
máximo cinco instâncias, devemos alterar o arquivo standalone-ha.xml contido no diretório do
WildFly na pasta standalone/configuration .
http://localhost:8080/fj25-financas-web/cicloDeVida.xhtml
No primeiro teste chame o Agendador pela interface web. Submeta o formulário uma vez. Depois
verifique a quantidade de instâncias criadas pelo EJB Container no console no Eclipse.
Repita o teste e submeta uma vez o formulário pela interface web. Repare que a quantidade de
instâncias não aumentou.
Agora teste várias chamadas, submeta o formulário várias vezes (mais do que 5 vezes). Depois fique
acompanhando o trabalho no WildFly pelo console.
Quantas instâncias foram criadas no Pool? O que acontece se temos 6 clientes querendo acessar o
Agendador ?
<stateless>
<!--<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>-->
</stateless>
Reinicie o WildFly e submeta o formulário algumas vezes (mais de duas). Verifique o console. Qual é
a politica de criação do Wildfly agora?
//@Stateless
@Singleton
public class Agendador {
Republique a aplicação no WildFly. Faça novamente alguns testes pela interface web. Repare que
apenas uma instância é criada.
O timeout padrão é de 500 milisegundos, um valor muito baixo para nossa execução. Use anotação
@AccessTimeout para aumentar o tempo limite de timeout.
CAPÍTULO 10
AGENDAMENTO DE TAREFAS
A aplicação nem precisa saber como foi criado o TimerService e onde ele está, pois o Java EE
Container injeta essa dependência nos Session Beans.
A primeira ideia é injetar o TimerService para usar ele no construtor e criar um javax.ejb.Timer:
@Singleton
public class Agendador {
@Resource
private TimerService timerService;
public Agendador() {
// Aqui ainda não aconteceu a injeção.
timerService.createTimer(10000L, "timeout"); // ERRO !!
}
@Timeout
public void atualiza(Timer timer) {
//chamado pelo container periodicamente
}
}
O código acima apresenta um erro comum que quase todo mundo que trabalha ou trabalhou com
EJB3 comete uma vez na vida: o TimerService foi utilizado no construtor, só que a injeção é feita
depois que o objeto foi construído, ou seja, depois do construtor.
Portanto, quando o construtor executa, o atributo timerService está null . Para solucionar essa
situação, podemos usar o callback @PostConstruct :
@Singleton
public class Agendador {
@Resource
private TimerService timerService;
@PostConstruct
public void posConstrucao(){
timerService.createTimer(10000L, "timeout"); // CERTO !!
}
@Timeout
public void agendado(Timer timer) {
//chamado pelo container periodicamente
}
Nesse caso o método agendado(..) anotado com @Timeout é chamado pelo container 10
segundos após o agendamento.
A String passada como argumento ao createTimer pode ser útil para que o método anotado
com @Timeout saiba qual timer gerou essa invocação. Há também a opção de passarmos mais
configurações como argumento, através de um TimerConfig , como veremos adiante.
Também pode fazer sentido inicializar o agendamento quando a aplicação será carregada. Podemos
aproveitar uma novidade do EJB 3.1 e configurar o bean Agendador com a anotação @Startup:
@Singleton
@Startup
public class Agendador {
Através da classe ScheduleExpression é possível definir horários exatos e intervalos com precisão.
Seguem alguns exemplos:
//toda segundas 9hs da manhã
ScheduleExpression expression = new ScheduleExpression();
expression.dayOfWeek("Mon");
expression.hour("9");
Podemos até configurar os segundos, timezone ou algum dia específico no mês. Mais exemplos se
encontram na documentação da classe ScheduleExpression :
http://download.oracle.com/javaee/7/api/javax/ejb/ScheduleExpression.html
Com isso nem é preciso usar a anotação @Timeout . O método anotado com @Schedule é
chamado pelo EJB container no horário definido.
Um Timer é persistido
O container EJB persiste os Timers em um banco de dados e recupera quando o servidor for
reiniciado. Se não desejamos persistir o agendamento, podemos definir isso usando um objeto do tipo
TimerConfig :
config.setPersistent(false);
this.timerService.createCalendarTimer(expression, config)
3. Implemente o método agenda , que nos permite escolher de quanto em quanto tempo o sistema
será acionado, de acordo com seus parâmetros:
ScheduleExpression expression = new ScheduleExpression();
expression.hour("*");
expression.minute(expressaoMinutos);
expression.second(expressaoSegundos);
this.timerService.createCalendarTimer(expression, config);
4. Escreva agora o método que será acionado cada vez que a regra do scheduler for válida. O método
pode ter qualquer nome, pode receber um objeto Timer como argumento e deve ser anotado com
@Timeout .
Caso você quisesse mandar o email semanal, poderia usar o seguinte código:
@Schedule(hour="9", minute="0", second="0", dayOfWeek="Mon", persistent=false)
public void enviaEmailCadaMinutoComInformacoesDasUltimasMovimentacoes() {
System.out.println("enviando email semanal");
}
CAPÍTULO 11
TRANSAÇÕES E EXCEÇÕES
"O primeiro problema para todos, homens e mulheres, não é aprender, mas desaprender" -- Gloria Steinem
Algumas operações são críticas, pois obrigatoriamente elas devem terminar com sucesso ou, se
algum erro ocorrer no meio do caminho, o estado inicial antes da operação começar precisa ser
recuperado.
Daí surge o conceito de transação. Uma transação deve garantir que os passos de uma operação
sejam totalmente executados ou, se um erro ocorrer, os passos já executados sejam "desfeitos".
Vamos exemplificar o poder do serviço de transação que o EJB Container oferece. Suponha que o
cliente fez uma chamada a um Session Bean, digamos que seja o DAO. Digamos que o sistema precisa
alimentar dois bancos de dados diferentes.
O EJB Container garante que todos os passos do processo acima descrito serão executados até o fim
ou desfeitos se algo ocorrer de errado em qualquer ponto do processamento.
Do ponto de vista de uma aplicação Java EE, o gerenciamento de transações pode ficar totalmente
transparente. Porém, se ela quiser fazer essa tarefa manualmente, o servidor oferece mecanismos para
facilitar esse processo.
Nos métodos de negócio, o Dao pode demarcar o código que deve ser executado dentro de uma
transação. Para isso, esse Session Bean precisa "conversar" com o UserTransaction que pode ser
obtido com injeção de dependência.
@Resource
private UserTransaction ut;
try {
this.ut.begin();
this.manager.persist(conta);
this.ut.commit();
} catch (NotSupportedException e) {
throw new EJBException(e);
} catch (SystemException e) {
throw new EJBException(e);
} catch (SecurityException e) {
throw new EJBException(e);
} catch (IllegalStateException e) {
throw new EJBException(e);
} catch (RollbackException e) {
throw new EJBException(e);
} catch (HeuristicMixedException e) {
throw new EJBException(e);
} catch (HeuristicRollbackException e) {
throw new EJBException(e);
} finally {
try {
if (this.ut.getStatus() == Status.STATUS_ACTIVE) {
this.ut.rollback();
}
} catch (IllegalStateException e) {
throw new EJBException(e);
} catch (SecurityException e) {
throw new EJBException(e);
} catch (SystemException e) {
throw new EJBException(e);
}
}
A complexidade dessa API é alta e o seu uso é desaconselhado, a não ser que tenhamos um caso
muito particular. A próxima estratégia será extremamente mais simples.
Transações gerenciadas pelo Session Bean deixam a responsabilidade de controle transacional para o
programador. Ele terá que abrir, consolidar (commit) ou desfazer (rollback).
Elas podem ser úteis caso você precise executar algum código associado ao rollback. Um outro
possível uso é para o caso de você querer uma transação dentro de uma classe que não recebe esse
serviço pronto do container. Um exemplo seria ter uma transação dentro de um servlet da sua aplicação.
O EJB Container fica com a responsibilidade de iniciar, consolidar (fazer commit) ou desfazer a
transação (rollback). O programador não pode e nem deve usar os métodos de controle transacional
nessa estratégia. Essa estratégia é conhecida como Container Managed Transaction - CMT.
O padrão é CMT mas se a aplicação quer deixar explícito que deseja utilizar o modo CMT ela pode
anotar a classe do Session Bean com a
@TransactionManagement(TransactionManagementType.CONTAINER) .
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class ContaDao {
// ...
}
Lembre que a anotação @TransactionManagement serve para forçar algum dos modos de controle
transacional e o padrão é CMT.
No modo CMT, o padrão é executar todos os métodos de negócio dentro de uma transação. O JEE
Container faz uma verificação toda vez que um método de negócio é invocado para saber se uma
transação já está aberta ou não. Se não houver transação aberta, o JEE Container abre uma nova antes de
começar a processar o método. A transação é fechada no final do método em que ela foi aberta. Em
outras palavras, se já existe uma transação aberta use-a, caso contrário, inicie uma nova. Essa estratégia é
conhecida como REQUIRED .
MANDATORY : Uma transação deve ser aberta antes do método ser chamado. Caso contrário,
lança uma TransactionRequiredException .
NEVER : Só pode executar sem transação. Se existir alguma aberta, uma exception é lançada.
REQUIRES_NEW : Sempre abre uma nova transação. Se existir alguma aberta, é suspensa
temporariamente até o método acabar.
Para configurar outro estilo, um Session Bean pode utilizar a anotação @TransactionAttribute
em cada método.
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void adiciona(Conta conta) {
this.manager.persist(conta);
}
Outra possibilidade, é definir um estilo diferente do REQUIRED , que é o padrão para todos os
métodos de um Session Bean. Basta anotar a classe em vez dos métodos.
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public class ContaDao {
// ...
}
EntityTransaction tx = manager.getTransaction();
tx.begin();
// ...
tx.commit();
É ilegal fazer essas chamadas se o EntityManager foi injetado para você, pois aí a transação
estará sendo gerenciada pelo servidor.
As exceptions de sistema representam erros de infraestrutura. Por exemplo, não foi possível abrir
uma conexão com algum Data Source. Em geral, são erros dos quais a aplicação não tem influência
direta.
O que acontece com a transação se uma exceção ocorrer? A resposta parece óbvia, o servidor deve
fazer um rollback, mas isso não é sempre verdade. Podem existir algumas exceções que não são erros
fatais e podem ser recuperadas, como por exemplo exceções relativas a problemas de validação.
O comportamento do EJB Container quando ocorre uma exception depende fortemente do tipo de
exception que ocorre (System Exception ou Application Exception).
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Por padrão, toda exception do tipo unchecked, ou seja, que herda de RuntimeException é
considerada uma System Exception.
Por exemplo, podemos criar uma própria exceção utilizada nas classes DAO:
@ApplicationException(rollback=true)
public class ValorInvalidoException extends Exception { ... }
Como discutido anteriormente, as exceptions unchecked por padrão são System Exceptions.
Entretanto, você tem a possibilidade de alterar esse padrão com a anotação @ApplicationException
que pode fazer uma unchecked ser Application Exception. Por exemplo:
@ApplicationException(rollback=true)
public class ValorInvalidoException extends RuntimeException {}
Essa unchecked exception causaria o rollback da transação. Uma System Exception teria o mesmo
efeito causando um rollback na transação. Porém, há duas diferenças importantes aqui: a primeira é que,
usando uma Application Exception, o cliente recebe exatamente a exceção ValorInvalidoException e
não a EJBTransactionRolledbackException ; a segunda é que o Session Bean não é eliminado
quando se usa Application Exception o que não acontece quando se usa System Exception. Dessa forma, o
cliente poderia fazer um tratamento dessa exceção e executar o Session Bean novamente.
É possível também fazer a declaração das exceções via XML. Para isso, no arquivo ejb-jar.xml ,
basta adicionar a seguinte configuração:
<application-exception>
<exception-class>fqn.da.minha.Classe</exception-class>
<rollback>true</rollback>
</application-exception>
manager.persist(movimentacao);
if(movimentacao.getValor().compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Movimentacao negativa");
}
}
Como a exceção que estamos forçando é uma unchecked (System Exception), o EJB Container
executará o rollback e a movimentação não será adicionada.
http://localhost:8080/fj25-financas-web/movimentacoes.xhtml
Tente cadastrar uma movimentação com valor negativo. Ao cadastrar recebemos uma
EJBException com a RuntimeException embrulhada, indicando que algo inesperado aconteceu.
Acesse o MySQL e faça um select na tabela de movimentações para verificar se realmente foi feito
um rollback.
@ApplicationException
public class ValorInvalidoException extends RuntimeException {
Tente novamente o cadastro de uma movimentação inválida! Perceba que agora receberemos uma
ValorInvalidoException e que a movimentação é adicionada no banco, ou seja, não há rollback.
5. Faça com que o rollback seja executado. Na anotação @ApplicationException coloque a opção de
rollback para true .
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public void adiciona(Conta conta) {
this.manager.persist(conta);
}
http://localhost:8080/fj25-financas-web/contas.xhtml
@Stateless
public class ContaDao {
@Resource
private UserTransaction ut;
//codigo omitido
}
this.manager.persist(conta);
try {
this.ut.commit();
} catch (Exception e) {
try {
this.ut.rollback();
} catch (Exception e1) {
throw new EJBException(e1);
}
throw new EJBException(e);
}
}
Gerenciar transações é algo complicado. O código acima ainda precisa de alguns finally para
garantir a invocação do rollback quando necessário. O servidor já faz isso para você ao usar CMT,
e essa deve ser sua preferência!
CAPÍTULO 12
"Quando se rouba de um autor, chama-se plágio; quando se rouba de muitos, chama-se pesquisa" --
Wilson Mizner
Escrevemos algo parecido com SQL mas estamos em um mundo completamente diferente. Onde
está o nome da tabela? Não precisamos saber, mesmo que a Conta tivesse sido anotada com @Table e
um nome de tabela diferente da convenção fosse informado, isso não seria importante. Quando
escrevemos from Conta estamos criando uma consulta que retorna todos objetos do tipo Conta .
Como isso será executado e outros detalhes de implementação ficam a cargo do EntityManager . Essa
maneira de executar consultas orientadas a objeto é conhecida como Java Persistence Query Language
mas é mais comumente chamada de JPQL (e HQL - Hibernate Query Language quando estamos
utilizando instruções específicas do Hibernate).
Por exemplo, uma operação importante que devemos ter no nosso sistema é a possibilidade de serem
listadas todas as movimentações de uma determinada conta. Vamos realizar essa consulta usando a
JPQL. Vamos colocar esse novo método na classe onde concentramos todos os métodos de persistência
relativos à Movimentacao , no caso a classe MovimentacaoDao . Para realizar as consultas, precisamos
do EntityManager que injetamos pela anotação @PersistenceContext :
@Stateless
@PersistenceContext
private EntityManager manager;
Essa maneira resumida de escrever a JPQL, é uma funcionalidade que o Hibernate nos oferece mas
não é garantido pela especificação então, a partir de agora, escreveremos da seguinte maneira:
select m from Movimentacao m where ...
Demos um apelido (alias) para os objetos da classe Movimentacao , no nosso caso, m, e é através
dele que acessamos as propriedades. O nosso método com a query alterada, recuperando todas as
movimentações da conta, fica da seguinte forma:
Perceba que escrevemos bem parecido com SQL, mas em nenhum momento nos preocupamos em
qual tabela estamos trabalhando, muito menos com nomes de colunas. Nossa consulta toda é baseada no
que sabemos dos nossos objetos.
Por exemplo, sabemos que a Movimentacao tem um atributo que se chama conta e este um atributo
que se chama id, com isso podemos caminhar por eles.
Para realizar a consulta invocamos o método createQuery que recebe uma JPQL e nos retorna um
objeto do tipo Query . Com este objeto podemos solicitar que agora seja feita a a listagem, no caso
invocamos o getResultList .
O problema aqui é que a consulta ainda está com um gosto de SQL, estamos comparando o id .
Seria mais interessante se pudéssemos passar para o JPQL o nosso objeto do tipo Conta e ele fosse
responsável por descobrir, que nesse caso, o relacionamento estava se dando pelo atributo id daquela
conta. Além do mais, sabemos que ficar concatenando queries pode nos levar a problemas grandes,
como SQL Injection.
Para resolver ambos os problemas, o JPQL tem uma maneira de definir parâmetros, bem similar ao
PreparedStatement . Na nossa consulta, queremos definir um parâmetro na query para indicar que no
Temos duas maneiras de fazer isso no JPQL: a primeira é idêntica ao PreparedStatement , vamos
definindo as posições dos parâmetros. Abaixo temos o método modificado.
public List<Movimentacao> listaTodasMovimentacoes(Conta conta) {
Query query = this.manager.createQuery("select m from Movimentacao m "
+ " where m.conta = ?1");
query.setParameter(1, conta);
return query.getResultList();
}
Essa primeira forma é conhecido como Positional Parameter Notation. O problema dela é que
quando temos muitos parâmetros fica fácil confundir-se com os números, com isso acabamos errando as
posições. A segunda maneira que podemos fazer é ir dando nomes aos parâmetros da query. Desse jeito,
teríamos o seguinte código:
public List<Movimentacao> listaTodasMovimentacoes(Conta conta) {
Query query = this.manager.createQuery("select m from Movimentacao m "
+ " where m.conta = :conta");
query.setParameter("conta", conta);
return query.getResultList();
}
Essa segunda maneira é conhecida como Named Parameter Notation. Com ela, fica mais fácil
identificar os parâmetros e a chance de troca de nomes é menor do que a de troca dos números. Para
manter a lista organizada, vamos sempre ordenar a listagem pela data de movimentação de forma
ascendente. Como a JPQL é uma espécie de SQL orientada a objetos, a grande maioria das cláusulas são
exatamente as mesmas, incluindo a ordenação:
public List<Movimentacao> listaTodasMovimentacoes(Conta conta) {
Query query = this.manager.createQuery("select m from Movimentacao m "
+ " where m.conta = :conta "
+ " order by m.data asc");
query.setParameter("conta", conta);
return query.getResultList();
}
Crie um novo método na classe MovimentacaoDao que recebe uma Conta e busca as
movimentações associadas a ela. Escreva uma JPQL para isso, devolvendo as movimentações
ordenadas por valor.
2. Vamos integrar essa nova pesquisa ao nosso sistema Web. Já há uma tela e um managed bean do JSF
preparados para exibir os dados, basta invocarmos nosso novo método.
@Inject
private MovimentacaoDao dao;
Depois procure na mesma classe o método lista e altere o método para que ele atribua à
movimentacoes o resultado de invocação ao novo método criado no DAO.
Implemente o método executando essa busca. Não esqueça de preencher os parâmetros na query.
Reinicie o WildFly (ou faça Full publish). Teste acessando a URL: http://localhost:8080/fj25-
financas-web/movimentacoesPorValorETipo.xhtml
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Dessa maneira estaríamos retornando a lista com as movimentações, não é bem isso que queremos.
Queremos apenas a soma dos valores e, para isso, vamos usar a função de agregação sum para realizar a
soma de todos valores do conjunto de movimentações retornado:
Se temos muitas consultas que retornam determinado tipo de objeto várias vezes no sistema, esse
código para fazer cast começa a ficar totalmente espalhado. Visando resolver esse problema, a JPA2
trouxe um novo tipo de objeto para realizarmos queries, que é o TypedQuery . Com ele, conseguimos
definir o tipo do objeto que vai ser retornado pela nossa query e, com isso, não precisamos ficar fazendo
os casts. Para conseguirmos esse objeto, temos que usar a sobrecarga do método createQuery do
EntityManager que recebe um segundo parâmetro indicando o tipo de retorno:
Repare que agora não precisamos mais fazer o cast, o próprio TypedQuery consegue inferir o tipo
de retorno.
Repare que fazer uma consulta que envolve duas entidades com JPQL simplesmente resume-se a
navegar pelos relacionamentos. Mais uma vez estamos trabalhando voltado aos nossos objetos. Onde
está o join? Não precisamos nos preocupar com as chaves. Se fossemos fazer a mesma consulta usando
SQL, teríamos algo como:
select
movimentac0_.id as id1_,
movimentac0_.conta_id as conta6_1_,
movimentac0_.data as data1_,
movimentac0_.descricao as descricao1_,
movimentac0_.tipo as tipo1_,
movimentac0_.valor as valor1_
from
Movimentacao movimentac0_ cross
join
Conta conta1_
where
movimentac0_.conta_id=conta1_.id
and conta1_.titular=?
Vamos agora adicionar o método que realiza a busca de todas movimentações de determinado titular
na MovimentacaoDao . Vamos usar a TypedQuery para dizer que queremos que o resultado seja uma
lista de movimentações:
public List<Movimentacao> buscaTodasMovimentacoesDaConta(String titular) {
String jpql = "select m from Movimentacao m where m.conta.titular=:titular";
TypedQuery<Movimentacao> query =
this.manager.createQuery(jpql, Movimentacao.class);
query.setParameter("titular", titular);
return query.getResultList();
}
Nossa query vai usar a função soma e filtrar por conta e tipo de movimentação:
select sum(m.valor) from Movimentacao m
where m.conta=:conta and m.tipoMovimentacao=:tipo
Para executar o JPQL, você pode usar uma TypedQuery recebendo BigDecimal para evitar o cast.
Como a consulta devolve apenas um valor simples, chame getSingleResult ao final. Um esboço:
@Inject
private MovimentacaoDao dao;
Procure o método calcula e altere para que ele invoque o calculaTotalMovimentado passando
conta e tipoMovimentacao como argumento e armazenando o resultado no atributo total .
Precisamos agrupar as movimentações de alguma forma para que possamos somar os valores de um
grupo de movimentações. Seguindo a semelhança de sintaxe com o SQL, vamos usar a cláusula group by
para poder reunir todas movimentações do mesmo tipo:
String jpql = "select m from Movimentacao m group by m.data";
Nós agrupamos as movimentações apenas por mês, portanto as movimentações de janeiro de 2009 e
janeiro de 2010 farão parte do mesmo grupo. Para diferenciar as movimentações também por ano vamos
agrupá-las usando a função year junto com a função month:
É importante ressaltar que as funções month e year não estão definidas nem no Hibernate
nem na JPA. Quando o Hibernate encontra uma função desconhecida no meio do JPQL ele
simplesmente delega para o Banco de Dados na esperança de ele entender o comando. No caso de
month e year , muitos bancos (incluindo o MySQL) suportam essa sintaxe. Mas cuidado que ela
não faz parte da especificação e pode eventualmente não funcionar em certos bancos de dados.
Vamos agora adicionar os filtros necessários para especificar de qual conta estamos buscando as
movimentações, além de informar o tipo de movimentação. Para isso, vamos usar o where antes do
group by.
String jpql = "select m from Movimentacao m "
+ "where m.conta = :conta and m.tipoMovimentacao = :tipo "
+ "group by year(m.data), month(m.data) ";
O resultado da consulta deve trazer o mês, o ano e o total movimentado dentro daquele mês e ano.
Usando o month para pegar o mês, year para pegar o ano e a função sum para somar os valores, fazemos
nossa projeção dentro do SELECT:
String jpql = "select month(m.data), year(m.data), sum(m.valor) "
+ "from Movimentacao m "
+ "where m.conta = :conta and m.tipoMovimentacao = :tipo "
+ "group by year(m.data), month(m.data) ";
Com a consulta criada, basta usarmos o EntityManager para realizar a listagem. Mas o que o
getResultList() devolve neste caso? O Hibernate não tem como saber o tipo de objeto que a lista vai
conter, não há uma classe ou entidade que represente o retorno de meses e valores somados. De alguma
maneira, ele precisa poder guardar um número variável de objetos em alguma estrutura flexível. Para
isso, ele usa um array de Objects para cada linha devolvida pelo resultado. Como estamos esperando
uma lista como retorno, temos uma estranha List<Object[]> :
public List<Object[]> listaMesesComMovimentacoes(Conta c, TipoMovimentacao tipo) {
String jpql = "select month(m.data), year(m.data), sum(m.valor) "
+ "from Movimentacao m "
+ "where m.conta = :conta and m.tipoMovimentacao = :tipo "
+ "group by year(m.data), month(m.data) ";
return query.getResultList();
}
Essa abordagem nos leva a um problema: quem invoca esse método precisa saber a ordem que os
valores estão posicionados em cada array dentro da lista, levando a um alto acoplamento do código com
a consulta.
Conta conta = new Conta();
conta.setId(1);
Se mudarmos a ordem do select da nossa consulta, vamos afetar quem invoca nosso método, fazendo
com que, possivelmente, alguma informação apareça errada para o usuário. Para diminuir esse
acoplamento, podemos criar uma nova classe para representar o retorno da consulta, ou seja, representar
o agrupamento de meses e valores. Não é uma nova entidade, é apenas uma classe simples que vai servir
simplesmente para guardar os valores e expor getters para acessá-los:
package br.com.caelum.financas.modelo;
Repare que temos que colocar o nome completo da nossa classe, ou seja, o nome do pacote mais o
nome da classe. Essa maneira de instanciar objetos dentro da JPQL é chamado de Constructor
Expressions. Esse objeto, que não é uma entidade, pode ser considerado como um Data Transfer Object,
pois serve apenas para transportar dados de uma maneira mais organizada, e pode ajudar na
granularidade, trazendo apenas os dados necessários.
Além disso, para organizar melhor o relatório, vamos ordená-lo de acordo com o total gasto no mês
de maneira decrescente:
public List<ValorPorMesEAno> listaMesesComMovimentacoes(Conta conta,
TipoMovimentacao tipoMovimentacao) {
String jpql =
"select new br.com.caelum.financas.modelo.ValorPorMesEAno"
+ "(month(m.data), year(m.data), sum(m.valor)) "
+ "from Movimentacao m "
+ "where m.conta = :conta and m.tipoMovimentacao = :tipo "
+ "group by year(m.data), month(m.data) "
+ "order by sum(m.valor) desc";
return query.getResultList();
}
Com a transformação do ValorPorMesEAno , nosso código de acesso fica muito mais simples e
claro:
List<ValorPorMesEAno> resultado =
movimentacaoDAO.listaMesesComMovimentacoes(conta, TipoMovimentacao.ENTRADA);
Não precisamos mais saber detalhes de implementação, como quantas posições tinham cada vetor da
lista e em quais posições estavam as informações. Podemos trabalhar com um objeto que faz muito mais
sentido para nosso domínio.
Dica: Use o Ctrl+3 para gerar o construtor e os getters. Para o construtor, digite o atalho gcuf e, para
os getters, ggas.
3. Nosso sistema possui uma tela e um managed bean do JSF já preparados para exibir nosso relatório.
Vá na classe MesesComMovimentacaoBean e crie um novo atributo chamado valoresPorMesEAno
do tipo List<ValorPorMesEAno> . Esse novo atributo armazenará a lista que vai ser devolvida pela
busca que criamos antes no DAO.
Por último, altere o método lista desse managed bean para que ele invoque nossa consulta
listaMesesComMovimentacoes no MovimentacaoDao e salve seu resultado no atributo
select
new br.com.caelum.financas.modelo.ValorPorMesEAno
(month(m.data), year(m.data),sum(m.valor))
from Movimentacao m
where m.conta = :conta and m.tipoMovimentacao = :tipo
group by year(m.data), month(m.data) having sum(m.valor) > :minimo
order by sum(m.valor) desc
@NamedQuery(name = "Movimentacao.buscaTodasMovimentacoesDaConta",
query = "select m from Movimentacao m
where m.conta.titular=:titular")
@Entity
Perceba que segue o mesmo padrão de um arquivo properties. Associamos uma chave através do
atributo name à uma query, esta especificada pelo atributo query . Agora no nosso método teríamos:
public List<Movimentacao> buscaTodasMovimentacoesDaConta(String titular) {
Query query = this.manager.createNamedQuery("Movimentacao.
buscaTodasMovimentacoesDaConta");
query.setParameter("titular", titular);
return query.getResultList();
}
Uma outra característica imposta pelo uso das Named Queries é o uso dos Named Parameter
Notation ou dos Positional Parameter Notation. Como não temos como concatenar a query com algum
valor dinamicamente, somos obrigados a usar uma das duas maneiras de setar os parâmetros, o que
impõe logo de início a boa prática. Além destes ganhos, no momento da criação da
EntityManagerFactory , as Named Queries já são parseadas para o SQL específico, fazendo com que a
aplicação possa vir a ter ganhos de performance quando muito utilizada.
CAPÍTULO 13
RELACIONAMENTO BIDIRECIONAL E
LAZYNESS
System.out.println(movimentacoes.size());
Pensando de uma maneira mais orientada a objetos, seria mais natural pedirmos a conta todas as
suas movimentações, ao invés de invocar um método em outro lugar. Se o código seguisse essa ideia
ficaria assim:
Conta conta = // recuperamos uma conta
System.out.println(conta.getMovimentacoes().size());
Seguindo essa linha, precisamos modificar nossa classe Conta para ter um atributo que possa
guardar todas as movimentações e disponibilizar os métodos de acesso para esse atributo. Modificando
// ...
private List<Movimentacao> movimentacoes;
}
Deixando dessa maneira, ainda não conseguimos falar para a JPA trazer todas as movimentações
associadas a nossa conta. Perceba que simplesmente criamos um atributo, nada de mais. Assim como
anotamos com ManyToOne quando queremos indicar um relacionamento de muitos-para-um, temos
uma anotação para indicar um relacionamento de um-para-muitos, ela se chama @OneToMany . Para
completarmos a configuração da nossa classe Conta vamos mapear o atributo. O código ficaria:
@Entity
public class Conta {
// ...
@OneToMany
private List<Movimentacao> movimentacoes;
}
Dessa forma, estamos configurando a JPA para que, quando carregarmos um objeto do tipo Conta ,
ele consiga as movimentações associadas.
@Entity
public class Conta {
// outra atributos aqui
@OneToMany
private List<Movimentacao> movimentacoes;
}
2. Pensando pelo lado relacional, adicionar esse novo mapeamento não deveria mudar nada nas tabelas
do banco. A tabela Movimentacao já tem a chave estrangeira para a tabela que guarda as contas.
Para verificar se tudo está correto, reinicie o WildFly agora. Ao subir o WildFly automaticamente
inicializa o JPA.
3. Acesse o MYSQL para verificar se as nossas tabelas sofreram alguma alteração ou se algo foi criado.
mysql -u root
use fj25;
show tables;
Perceba que uma tabela com nome Conta_Movimentacao foi criada. Por quê?
Para os nossos objetos, essa relação não é implícita. Não é porque temos um atributo do tipo Conta
na classe Movimentacao , que automaticamente temos um atributo do tipo List<Movimentacao> na
classe Conta , tivemos que fazer isso manualmente. Perceba que o inverso também não é obrigatório,
não é porque temos um List<Movimentacao> na Conta que toda movimentação tem um atributo do
tipo Conta .
É exatamente dessa maneira que a JPA entende. Para ele, existem duas relações, uma de muitos para
um da movimentação para a conta e outra de um para muitos da conta para as movimentações. Para
representar esse novo mapeamento, o Hibernate criou essa nova tabela Conta_Movimentacao .
O que queremos então é configurar a JPA para que ela entenda que esse novo relacionamento
representa o mesmo que já temos configurado com a anotação ManyToOne na classe Movimentacao .
Para atingir esse objetivo, devemos usar um atributo da anotação OneToMany , cujo nome é mappedBy ,
que indica que essa relação é a mesma representada pelo atributo conta na classe Movimentacao :
@Entity
public class Conta {
@OneToMany(mappedBy="conta")
private List<Movimentacao> movimentacoes;
// getters e setters
}
Perceba que a classe que não possui o mappedBy mapeado no atributo é a que determina o modelo
de banco dados, por isso ele é chamado de owning entity ou lado forte da relação. A outra, que utiliza o
mappedBy para informar que aquele relacionamento já está mapeado na outra ponta, é chamada de
inverse entity ou lado fraco da relação.
Devemos evitar o uso impensado de getters e setters. Nossas entidades podem sim, e devem, possuir
regra de negócio que sejam de sua responsabilidade. Veremos no decorrer do curso como enriquecer
essas entidades, a fim de evitar o modelo anêmico e expor menos nossos atributos internos.
2. Vá até o terminal e acesse o MySQL novamente. Delete a tabela de relacionamento criada pelo
Hibernate:
drop table Conta_Movimentacao;
3. Agora, reinicie o WildFly pelo Eclipse. Acesse novamente o MySQL e veja que nada de novo foi
criado. Ensinamos à JPA que o nosso relacionamento um para muitos da conta para as
movimentações, representa o mesmo do atributo conta na classe Movimentacao .
Quando estamos utilizando um mapeamento bidirecional, ou seja a mesma relação mapeada nas
duas pontas, temos que tomar alguns cuidados. Por exemplo, imagine o seguinte o código:
Conta conta = // recupera uma conta sem movimentacoes
Pensando naturalmente, quando mostramos quantas movimentações estão associadas à nossa conta,
nesse momento, deveria ser impresso um, mas aparece zero. Como não existe nenhuma relação
assumida automaticamente, temos que, manualmente, adicionar essa movimentação na lista de
movimentações da conta. Então teríamos que fazer o seguinte:
movimentacao.setConta(conta);
conta.getMovimentacoes().add(movimentacao);
Para representar essa possibilidade no nosso sistema, primeiramente precisamos de uma classe que
represente a categoria. Queremos gravar essas categorias criadas no banco de dados, para isso nossa
classe também precisa ser mapeada como uma entidade e gerenciada pelo JPA. Dessa maneira temos a
seguinte classe:
@Entity
public class Categoria implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String nome;
Agora, precisamos informar a JPA que a movimentação pode possuir diversas categorias
relacionadas. Poderíamos pensar num relacionamento do tipo um para muitos, o problema dessa
abordagem é que cada categoria seria de apenas uma movimentação e, como provavelmente teríamos
duas movimentações associadas a mercado por exemplo, acabaríamos com várias categorias com nomes
repetidos cadastradas. Precisamos informar que uma movimentação pode ter várias categorias
associadas mas, ao mesmo tempo, essas mesmas categorias podem estar associadas a outras
movimentações, sem duplicar esses cadastros. Isso caracteriza um relacionamento do tipo muitos para
muitos. Primeiro precisamos alterar a classe Movimentacao para indicar que ela contém uma lista de
categorias associadas. Ficamos com o código da seguinte maneira:
@Entity
public class Movimentacao {
// outros atributos
}
Temos também que informar a JPA que essa lista representa um relacionamento muitos para
muitos. Para isso, vamos utilizar a anotação ManyToMany :
@Entity
public class Movimentacao {
@ManyToMany
private List<Categoria> categorias = new ArrayList<Categoria>();
// ...
}
Pensando no banco de dados, não temos como representar esse tipo de relacionamento apenas com
duas tabelas. Temos apenas um cadastro da movimentação e um cadastro da categoria e precisamos
dizer que essa mesma movimentação está relacionada com várias categorias e vice-versa.
Para representar isso, é utilizada uma tabela que, geralmente, contém apenas duas colunas: uma para
o id de uma ponta da relação, no nosso caso a movimentação e outra para a outra ponta da relação, no
nosso caso a categoria. Essa tabela é chamada de Tabela de Relacionamento, ou tabela associativa. O
único objetivo é relacionar registros de uma tabela com os registros da outra. É justamente essa tabela
que vai ser gerada pelo Hibernate para representar nosso mapeamento de muitos para muitos. Pela
convenção, vai ser gerada uma tabela chamada Movimentacao_Categoria , ou seja, o nome da classe
que contém o relacionamento e depois o nome da classe que foi associada. É interessante notar que, mais
uma vez, em nenhum momento vamos precisar fazer referência a essas tabelas que foram criadas para
nós. Sempre trabalhamos baseado nos nossos objetos!
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String nome;
2. Sobrescreva também o método toString na classe Categoria para retornar o nome da categoria:
@Entity
public class Categoria implements Serializable {
@ManyToMany
private List<Categoria> categorias = new ArrayList<Categoria>();
4. Reinicie o WildFly. Após subir, o banco de dados deve ser atualizado. (Caso precise, faça um Full
Publish do projeto fj25-financas-web )
5. Antes de cadastrar as categorias associadas a cada movimentação, é preciso preparar os dados para a
tela. Precisamos listar todas as categorias e buscar uma categoria pelo id. Para isolar essa operação,
vamos criar um DAO para a categoria. No pacote br.com.caelum.financas.dao crie a classe
CategoriaDao , use a anotação @Stateless para indicar um Session Bean e injete o
EntityManager com a anotação @PersistenceContext :
package br.com.caelum.financas.dao;
@Stateless
public class CategoriaDao implements Serializable {
@PersistenceContext
private EntityManager manager;
6. Vamos utilizar este DAO na classe MovimentacoesBean . Abra a classe e injete o CategoriaDao :
@Inject
private CategoriaDao categoriaDao;
7. A nossa tela vai chamar na classe MovimentacoesBean uma ação para associar uma categoria com a
movimentação a cadastrar.
Para isso funcionar, vamos criar um método adicionaCategoria que busca a categoria pelo id e
passa para a movimentação (o id da categoria vem do formulário):
public void adicionaCategoria() {
if(this.categoriaId != null && this.categoriaId > 0){
Categoria categoria = categoriaDao.procura(this.categoriaId);
this.movimentacao.getCategorias().add(categoria);
}
}
8. O formulário da movimentação terá uma combo box para mostrar todas as categorias disponíveis.
Para tal vamos adicionar um novo atributo na classe MovimentacoesBean que representa todas as
categorias.
Adicione o atributo:
Também gere o getter para o atributo. No getter vamos utilizar o DAO para buscar as categorias:
10. Não temos categorias cadastradas no banco de dados. Para isso abra o terminal e insira as seguintes
categorias no MySQL:
insert into Categoria(nome) values('gasto fixo');
insert into Categoria(nome) values('gasto variavel');
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
@ManyToMany
@JoinTable(name = "categorias_da_movimentacao")
private List<Categoria> categorias = new ArrayList<Categoria>();
É interessante notar que dois selects foram realizados, um primeiro para simplesmente carregar a
movimentação com id especificado e um segundo, no momento que invocamos pela primeira vez o
método getNome() da primeira categoria iterada pelo for . Para tentar otimizar ao máximo o
desempenho da aplicação, o Hibernate só vai realizar as queries quando realmente for necessário. Esse
comportamento é conhecido como lazy load ou carregamento preguiçoso.
Usando essa abordagem, sempre que buscarmos uma movimentação traremos também suas
categorias. Se existirem muitos locais no sistema nos quais precisamos de todos os dados da
movimentação exceto as categorias, adotar a estratégia EAGER para categorias será uma boa alternativa?
Como vimos anteriormente, o Hibernate adota aqui o lazy load como comportamento default, então
no momento que invocamos o getCategorias ele vai tentar disparar a query para realizar a busca. O
problema que temos aqui, é que nesse momento o EntityManager já foi fechado. Repare na imagem
abaixo que o EntityManager está sendo gerenciado pelo container EJB. A movimentação na view não
está mais gerenciadada e e sim no estado Detached. Não sendo mais gerenciado fica impossível a
realização da query.
Consequentemente, quando tentarmos exibir as categorias na nossa view, uma exception vai ser
disparada informando que não foi possível a realização do lazy load. No caso do Hibernate, essa
exception tem o nome de LazyInitializationException .
@Stateless
public class MovimentacaoDao {
@PersistenceContext
private EntityManager manager;
//método omitidos
}
Enquanto trabalhamos com esse EntityManager dentro de um EJB Session Bean temos a garantia
de ter uma conexão e transação aberta.
Para fugir do problema do lazy load uma estratégica adotada do mercado é inicializar todos os dados
antes de passar para a tela (view), ou seja inicializar todos os dados dentro do Session Bean. A ideia é
planejar antecipadamente quais entidades e relacionamentos serão utilizados, e assim só devolver dados
já carregados sem causar o problema do lazy load.
Para o resolver o nosso problema o JPQL oferece uma forma de pedir explicitamente o carregamento
dos relacionamentos. Isto é feito através de um join explicito junto com a clausula fetch . Por
exemplo podemos carregar todas as movimentações com as categorias:
public List<Movimentacao> lista() {
return manager.createQuery("select m from Movimentacao m "
+ " join fetch m.categorias", Movimentacao.class).
getResultList();
}
Porém ao executarmos a query vemos que nossos registros estão repetidos. Isto acontece porque para
cada linha em categorias serão trazidas todas as movimentações onde aquela categoria está relacionada.
Mas se uma movimentação tem mais de uma categoria, isso acaba causando a repetição desses registros
em nossa consulta. Para trazer apenas uma vez cada uma das movimentações encontradas, usamos a
cláusula distinct :
select distinct m from Movimentacao m join fetch m.categorias
Dessa maneira, estaríamos trazendo apenas as movimentações que possuem categorias, se quizermos
trazer todas, inclusive as que não tem categorias, poderíamos usar um LEFT JOIN. Então ficaria da
seguinte forma:
select distinct m from Movimentacao m left join fetch m.categorias
Assim que executamos a query, automaticamente serão carregadas as categorias. No entanto nem
sempre queremos todas as movimentações juntas com as categorias e por isso faz sentido criar um outro
método na classe MovimentacaoDao com essa responsabilidade e nome específico:
Aqui fica visível a desvantagem dessa abordagem. Naturalmente vamos escrever mais queries e
menos utilizar o nosso modelo OO. O código se torna mais procedural o que pode prejudicar a
manutenção ao longo prazo.
@ManyToMany(fetch=FetchType.EAGER)
private List<Categoria> categorias = new ArrayList<Categoria>();
Dessa maneira, sempre que buscarmos por uma movimentação, automaticamente já vai ser
carregada também a lista com todas as categorias. Nesse caso, devemos tomar cuidado com exageros.
Muitas pessoas acabam mudando todos seus relacionamentos para EAGER , gerando um excesso de
carregamentos de entidades totalmente desnecessário.
Uma boa política de aquisição de entity managers dentro de uma aplicação web é uma por requisição.
Uma ideia é interceptar toda requisição ao nosso servidor e, antes de mais nada, criar um novo entity
manager, juntamente com uma transação. Depois que todo fluxo é executado devemos fazer o commit e
fechar o entity manager corrente. Deixaremos o entity manager aberto durante toda a requisição,
inclusive durante a renderização da nossa view, para o caso da utilização de coleções lazy. Assim
podemos resolver o problema do lazy load!
Para atingirmos esse objetivo, poderiamos utilizar servlet filters que abrirão e fecharão nossos _entity
manager_s mas isso só funcionaria em uma aplicação sem EJB. Nesse caso gerenciaríamos o entity
manager dentro do Servlet Container sem um segundo container. Isso também significa que estamos
responsáveis pela abertura e commit das transações. Ou seja, perderíamos a inversão de controle que o
container EJB oferece.
Vamos continuar com a ajuda do EJB mas usando o EntityManager per request pattern ou seja, um
EntityManager por requisição. Para implementar este padrão podemos aproveitar o CDI. Ele vai
gerenciar a abertura e fechamento dos EntityManager s. A JPA e a transação continuarão nas mãos do
container EJB, mas o CDI é quem vai disponibilizar o EntityManager para a aplicação.
Nela vamos colocar dois métodos, um para abrir ( getEntityManager ) outro para fechar ( close ).
No getEntityManager vamos abrir o manager pela EntityManagerFactory como já foi visto.
Repare também que o método close recebe o EntityManager :
A EntityManagerFactory será injetada pelo CDI. Para isso existe uma anotação específica.
Analogo ao @PersistenceContext para injetar um EntityManager , existe a anotação
@PersistenceUnit para uma EntityManagerFactory :
@PersistenceUnit
private EntityManagerFactory factory;
@PersistenceUnit
private EntityManagerFactory factory;
@Produces @RequestScoped
public EntityManager getEntityManager(){
return factory.createEntityManager();
}
Repare que a EntityManagerFactory continua sendo gerenciada pelo container EJB porém
abrimos o EntityManager pelo CDI. Este tipo de EntityManager também é chamado de Application
Managed Entity Manager.
Nos DAOs devemos usar o novo EntityManager que está sendo produzido com a ajuda do CDI.
Para isso basta trocar a anotação @PersistenceContext por @Inject :
@Stateless
public class ContaDao {
@Inject //@PersistenceContext
private EntityManager manager;
Isso já é suficiente para usar um EntityManager que vive apenas por uma requisição, ou seja, estará
aberto enquanto a view está sendo renderizada, seguindo assim o padrão chamado
OpenEntityManagerInView, porém este EntityManager não participa automaticamente da transação
gerenciada pelo container EJB. Nos métodos que precisam de transação devemos chamar
manager.joinTransaction(), por exemplo:
http://docs.jboss.org/hibernate/stable/entitymanager/reference/en/html_single/#transactions-
basics-uow
Esse padrão no Hibernate era conhecido analogamente como Open Session In View.
Vamos implementar dois métodos getEntityManager e close que devem criar e fechar um
EntityManager :
@Produces @RequestScoped
public EntityManager getEntityManager(){
return factory.createEntityManager();
}
Abra as classes Dao e substitua a anotação @PersistenceContext pela @Inject , por exemplo:
@Stateless
public class ContaDao {
@Inject //@PersistenceContext
private EntityManager manager;
4. Abra a classe MovimentacaoDao . Nos métodos que precisam que o EntityManager participe de
uma transação, chame o método manager.joinTransaction() .
http://localhost:8080/fj25-financas-web/movimentacoes.xhtml
A outra forma é fazer o join fetch , forçando essa query a já retornar tudo de conta, como aqui:
Veremos, mais para a frente, o cache de segundo nível. Ele também ajudaria nesse caso, pois cada
Conta poderia já ter sido cacheada durante o uso de outro EntityManager .
CAPÍTULO 14
Uma das formas mais efetivas da JPA para otimizar a performance da nossa aplicação é através de
caches dos dados que trazemos, evitando assim muitas chamadas ao banco de dados.
A maneira mais simples de cache que existe na JPA é a que os EntityManager s possuem
internamente. Nesse caso, o EntityManager evita que carreguemos duas vezes a mesma informação do
banco de dados.
Com isso, ao invocarmos um find no EntityManager duas vezes para trazer o mesmo objeto,
este só vai até o banco de dados na primeira vez. Quando o find é invocado pela segunda vez no
mesmo EntityManager ele sabe que aquele objeto já foi carregado anteriormente e reaproveita os
dados já carregados sem fazer a segunda comunicação com o banco de dados.
Essa camada, entre cada EntityManager e o banco de dados, é o chamado cache de primeiro nível.
Abra a classe CacheBean . Nela injete diretamente o EntityManager , ou seja sem o DAO:
@Inject
private EntityManager manager;
No formulário digite um id de uma conta que exista no banco de dados. Veja a saída no console.
Quantos selects foram feitos? Era para acontecer isso mesmo?
Nesse caso, o mais vantajoso seria um cache que mantivesse as informações entre vários
EntityManagers . Dessa forma, usuários diferentes que enviassem requisições distintas
compartilhariam o mesmo cache. Se ambos precisassem ler a mesma informação, ela só seria buscada no
banco de dados uma única vez. E se depois disso a informação precisasse ser lida por outros usuários
novamente, não haveria uma nova comunicação com o banco de dados.
Mas se a informação não é buscada do banco de dados, de onde ela vem então? Precisamos de um
espaço de cache que seja compartilhado entre os EntityManager e que seja utilizado quando o cache
de primeiro nível não tiver a informação desejada. Esse é o cache de segundo nível.
O cache de segundo nível é bem mais crítico (e complexo) que o de primeiro nível, já que a
possibilidade de lidar com dados desatualizados (stale) é bem maior. Caso haja algum outro sistema
atualizando os dados sem passar pelo Hibernate, pode inviabilizar completamente o seu uso. Esse cache
vem desabilitado por padrão, mas costuma ser essencial para o bom andamento de uma aplicação.
Há diversas outras implementações de cache que se encaixam bem com a JPA e com o Hibernate,
seguindo suas APIs. Uma que tem ganhado bastante destaque é o Infinispan, disponível no WildFly.
O Infinispan é um banco NoSQL com um foco grande em escalabilidade. Veja mais no site do
projeto:
http://infinispan.org
O WildFly já vem por padrão com o Infinispan, sendo assim, não precisamos configurá-lo como
provider de cache para uma persistence-unit dentro do persistence.xml .
@Entity
@Cacheable
public class Movimentacao implements Serializable {
// código omitido
}
Precisamos ainda fazer uma configuração fora do persistence.xml para usar o Infinispan do
WildFly: adicionar a dependência do mesmo no arquivo MANIFEST.MF, que está na pasta
WebContent/META-INF. Assim, o Hibernate consegue encontrar as classes necessárias para o
funcionamento do cache de segundo nível.
Dependencies: ... org.infinispan
Vamos informar ao Hibernate que queremos que os objetos da classe Conta sejam mantidos no
cache de segundo nível. Para isso vamos anotar a classe com @Cacheable .
@Entity
@Cacheable
public class Conta {
//atributos e métodos omitidos
}
Como estamos usando o WildFly, ao invés de colocar um jar em nosso projeto, podemos apenas
dizer para o WildFly que queremos usar o Infinispan interno do servidor.
Isso deve ser feito adicionando a dependência do Infinispan no arquivo MANIFEST.MF, que está
na pasta WebContent/META-INF. (No nosso projeto este arquivo já está com a dependência
configurada)
Teste novamente o formulário digitando o id de uma conta que exista no banco de dados. Busque
algumas vezes a mesma conta. Veja a saída no console. Dessa vez, quantos selects foram feitos?
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL)
@OneToMany(mappedBy = "conta")
private List<Movimentacao> movimentacoes = new ArrayList<Movimentacao>();
INFINISPAN E CACHECONCURRENCYSTRATEGY
Há alguns detalhes para a invalidação do cache de uma coleção. Você precisa remover a entidade da
coleção cacheada, caso contrário essa coleção não será invalidada. Veja mais em:
http://planet.jboss.org/post/collection_caching_in_the_hibernate_second_level_cache
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
@Inject
private ContaDao dao;
5. Vamos melhorar isso adicionando cache na lista de movimentações que temos dentro da classe
Conta . Como esta é uma anotação específica do Hibernate, precisamos colocá-lo no classpath.
Como estamos usando o WildFly, ao invés de colocar um jar em nosso projeto, podemos apenas
dizer para o WildFly que queremos usar o Hibernate interno do servidor. Assim evitamos conflitos
com possíveis versões diferentes do hibernate sendo carregadas em nosso projeto.
Isso deve ser feito adicionando a dependência do Hibernate no arquivo MANIFEST.MF, que está na
pasta WebContent/META-INF. (No nosso projeto este arquivo já está com a dependência
configurada)
Vamos anotar também a classe Movimentacao para informar que os objetos deste tipo devem ser
cacheados. Para isso, simplesmente adicione a anotação @Cacheable da mesma forma que
fizemos na Conta .
Reinicie o servidor.
Acesse novamente a página para verificar a quantidade de movimentações de uma Conta e
pesquise várias vezes pela mesma Conta . Observe quantas queries são realizadas. Mudou algo?
6. Quando será que o cache é invalidado? Adicione mais uma movimentação na conta que você está
pesquisando.
A api é bem simples e possui apenas métodos para tirar (evict) objetos do cache. Por exemplo, para
invalidar uma conta com o ID 3:
cache.evict(Conta.class, 3);
cache.evict(Conta.class);
Vale lembrar que o ideal é sempre deixar o gerenciamento para o seu provedor de cache. Quanto
menos você precisar fazer controle fino da invalidação, melhor. Para isso, uma boa configuração do
tempo de vida do objeto e política de invalidação (como o LRU) podem ajudar bastante.
Podemos tornar o Cache disponível para a nossa aplicação criando um Producer do CDI, similar
ao producer de EntityManager que criamos anteriormente.
@PersistenceUnit
private EntityManagerFactory factory;
@Produces @ApplicationScoped
public Cache getCache() {
return factory.getCache();
}
}
Altere o método invalidar para tirar uma entidade do cache do segundo nível:
public void invalidar() {
cache.evict(Conta.class, id);
}
3. Vamos executar um select de uma conta, depois invalidar o cache e em seguida fazer um select para a
mesma conta.
Dessa forma, se executássemos a query from Conta conta where conta.titular like
:pTitular onde o parâmetro pTitular fosse "João" e ela retornasse 3 objetos, com ids 1, 7 e 10,
seria armazenado no cache a query executada, o parâmetro passado e também os ids dos objetos
retornados.
Esse cache sempre é invalidado quando acontece alguma operação em alguma das entidades
relacionadas, como um insert, update ou delete, portanto indicado para queries que envolvem tabelas
com muito mais leitura do que escrita.
Habilitamos o cache de queries através da invocação do método setHint , no objeto Query que
utilizamos dentro dos nossos DAOs para realizar as pesquisas. O método setHint recebe dois
parâmetros, o primeiro é uma chave que indica que estamos configurando o cache da consulta e o
segundo indicando que realizaremos o cache. Com isso, podemos aplicar o cache de query como no
código abaixo:
public List<Movimentacao> listaMovimentacoesPorValorETipo(
BigDecimal valor, TipoMovimentacao tipo) {
String jpql = "select m from Movimentacao m where m.valor <= :valor "
+" and m.tipoMovimentacao = :tipo";
Query query = this.em.createQuery(jpql);
query.setParameter("valor", valor);
query.setParameter("tipo", tipo);
query.setHint("org.hibernate.cacheable", "true");
return query.getResultList();
}
2. Abra o MovimentacaoDao e altere o método listaPorValorETipo para definir o Hint para que
a consulta seja cacheada. Para isso, adicione a chamada
query.setHint("org.hibernate.cacheable", "true"); .
query.setHint("org.hibernate.cacheable", "true");
return query.getResultList();
}
6. Insira uma nova movimentação, e refaça a query. Ela foi disparada novamente? Por quê?
<property name="hibernate.cache.use_second_level_cache"
value="true"/>
<property name="hibernate.cache.provider_class"
value="org.hibernate.cache.EhCacheProvider"/>
A primeira linha diz que queremos habilitar o cache de segundo nível, que por padrão vem
desabilitado, e a segunda linha indica qual é o provider que será utilizado, que no nosso caso é o
EhCache.
Mas uma das questões mais difíceis ao se trabalhar com Cache de segundo nível é determinar
quando o dado que está no cache deve ser invalidado. Para isso, as bibliotecas de Cache precisam saber
qual estratégia utilizar para invalidar o Cache. Basicamente existem quatro formas: READ_ONLY ,
NONSTRICT_READ_WRITE , READ_WRITE e TRANSACTIONAL .
A estratégia mais restritiva é a READ_ONLY , indicada para entidades que são apenas lidas e nunca
modificadas. Este é o modo mais simples, pois nunca irá se preocupar com sincronização e locks para os
objetos, já que não há escritas.
NONSTRICT_READ_WRITE é a mais comum, indicada para objetos que são raramente atualizados e
que têm probabilidade de atualizações simultâneas muito baixa. Caso muitas atualizações ocorram
"simultaneamente" pode ser que a última versão comitada no banco não seja a mesma que acabou
ficando no seu cache.
READ_WRITE é mais abrangente e suporta escrita mais frequente, garantindo que a última versão do
cache é a mesma do banco.
Você pode ainda configurar detalhes sobre o cache, utilizando o ehcache.xml no classpath do seu
projeto:
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="1000"
eternal="true"
overflowToDisk="false"
/>
<cache name="br.com.caelum.financas.modelo.Conta"
maxElementsInMemory="2000"
eternal="true"
overflowToDisk="false"
/>
</ehcache>
Existem opções para você configurar a política de cache, timeout e outros, através do arquivo de
configuração ehcache.xml . Veja mais no site do projeto:
http://ehcache.org
Caso você não defina nada no ehcache.xml por padrão serão criados caches que cabem até 10 mil
elementos, e esses elementos serão invalidados após dois minutos de existência. Esses defaults podem
mudar de versão em versão do ehcache.
Primeiramente para conseguirmos trabalhar com o cache de segundo nível num ambiente
standalone (sem servidor de aplicação), temos que habilitar um provider. No curso utilizaremos o
EhCache , mas existem outros disponíveis que podem ser utilizados.
O EhCache é uma biblioteca externa portanto precisamos trazer seu JAR para nossa aplicação.
Copie os JARs da pasta Atalho para arquivos dos cursos/25/ehcache para a pasta lib do projeto
fj25-financas e adicione no Build Path .
Vamos informar ao Hibernate que queremos que os objetos da classe Conta sejam mantidos no
cache de segundo nível. Para isso vamos anotar a classe dizendo qual a estratégia de cache que
vamos aplicar aos objetos.
@Entity
@Cache(usage=CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Conta {
//atributos e métodos omitidos
}
primeiraEM.getTransaction().begin();
Conta primeiraConta = primeiraEM.find(Conta.class, 1);
primeiraEM.getTransaction().commit();
primeiraEM.close();
Execute a classe e veja o resultado. Qual é a diferença entre o cache de primeiro nível e o de
segundo nível?
Configure para não usar o cache de segundo nível (comentando a annotation @Cache da classe
Conta ), rode, e verifique a diferença.
2. Quando será que o cache é invalidado? Tente alterar algum dado da Conta entre as duas pesquisas.
Basicamente, o EhCache possui 3 formas de decidir como ele deve tratar os objetos que já estavam
no cache para abrir espaço para novos objetos:
O padrão do EhCache é a utilização do algoritmo LRU, onde o objeto menos utilizado recentemente
é descartado do cache para dar espaço ao novo objeto que entrará no espaço do cache.
A estratégia LFU verifica qual o objeto que tem menor frequência de uso para poder ser descartado
do cache, enquanto o FIFO simplesmente remove o mais antigo.
Muitos dos problemas só podem ser descobertos através de uma análise ou de uma auditoria sobre o
uso do banco de dados e também do Hibernate e suas ferramentas. Justamente para facilitar esse
trabalho, o Hibernate possui uma API que gera estatísticas de uso da aplicação enquanto ela está em
execução.
Por padrão as estatísticas não são colhidas, pois isso demanda mais recursos o que pode fazer com
que a aplicação tenha uma sensível perda de performance. No entanto, para habilitar as estatísticas, basta
adicionar uma nova propriedade no persistence.xml :
Com isso conseguimos colher as estatísticas no momento que precisarmos. Para isso, basta
utilizarmos a EntityManager e invocarmos o método unwrap , que nos retornará um objeto interno
do Hibernate, que é a Session . Com esse objeto Session recuperado, podemos invocar o método
getSessionFactory que retornará a SessionFactory do hibernate, e então podemos invocar o
método getStatistics que retornará um objeto do tipo Statistics :
EntityManager em = // recuperando uma entitymanager
Session session = em.unwrap(Session.class);
Statistics stats = session.getSessionFactory().getStatistics();
A classe Statistics possui diversos métodos que podemos utilizar para recuperar informações
sobre o uso do Hibernate, como a quantidade de vezes que utilizamos o cache de segundo nível, quantas
vezes fizemos exclusões em uma determinada entidade, quantas queries foram cacheadas, qual a
consulta que demora mais tempo para executar e assim por diante.
@Inject
private EntityManager manager;
Lembre-se: precisamos criar os métodos getEstatisticas e setEstatisticas para que a view consiga
encontrar os dados para exibir.
3. Na mesma classe EstatisticasBean abra o método gera . Esse método é responsável por pegar
as estatísticas de uso do Hibernate e disponibilizar o objeto Statistics para ser exibido na interface.
Veja se algum número dentre os exibidos está estranho. Tente entender o por quê e como corrigir.
5. Qual seria um número bom para as conexões abertas e fechadas? Será que deveriam ser valores
parecidos ou diferentes? E os números de hits no cache de segundo nível? Um valor bom é um valor
alto? Ou baixo?
Mas por que essa abordagem poderia ser útil? Imagina que no nosso sistema a criação de uma conta
é um processo que envolve não só uma página, e sim 3 páginas diferentes. Depois de cada ação das
páginas queremos gravar/atualizar os dados da conta no banco, ou seja, precisamos nos preocupar como
deixar a conta gerenciada entre requisições diferentes.
Na nossa aplicação implementamos o padrão Open EntityManager In View . Isso significa que
para cada requisição:
Aplicando no código:
// em cada requisição
EntityManager manager = new JPAUtil().getEntityManager();
manager.getTransaction().begin();
manager.getTransaction().commit();
manager.close();
Sabendo que o mesmo EntityManager poderia ser aproveitado entre transações diferentes, seria
mais fácil abrir apenas um para todo o processo de criação/atualização da conta. Em cada requisição,
não queremos mais criar um novo EntityManager , mas abrir apenas uma nova transação e usar o
mesmo EntityManager para todo o processo. Queremos estender a vida do EntityManager
(Extended Persistence Context).
manager.close();
Isso é apenas um exemplo simples para mostrar o conceito. Para usar um Persistence Context
Extended na nossa aplicação é necessário que nosso filtro saiba gerenciar EntityManager entre
requisições diferentes, muito mais complicado do que nos fizemos. Por isso, e outros motivos, é comum
usar para o gerenciamento do EntityManager e transações, um container mais sofisticado. Exemplos
de container são: JBoss Seam e EJB3 Container. Ambos são aptos para trabalhar com Extended
Persistence Context .
Voltando no nosso exemplo da aplicação, não queremos mais persistir os dados em cada requisição.
Na verdade, queremos persistir os dados quando finalizarmos o processo de alteração da conta ou seja
no fim do processo.
Com apenas um EntityManager ativo fica simples de implementar. Vamos usar um Persistence
Context Extended , mas apenas na última requisição usarmos uma transação, conforme a figura
abaixo:
manager.close();
Há três opções para lidar com o problema. A primeira opção é não fazer nada e deixar sempre que o
último a ser salvo ganhe. As outras duas opções providas pelo Hibernate são: locks otimistas e locks
pessimistas.
O lock pessimista parte do princípio de que frequentemente vai haver problema de concorrência.
Portanto, para se precaver do problema, o Hibernate pede uma trava (lock) do objeto ao banco de dados.
O método lock() do EntityManager serve justamente para pedir esta trava ao banco de dados. Veja
o exemplo abaixo que usa dois EntityMangager para a mesma conta:
EntityManager em1 = new JPAUtil().getEntityManager();
EntityManager em2 = new JPAUtil().getEntityManager();
em1.getTransaction().begin();
em2.getTransaction().begin();
A grande desvantagem desta abordagem é que não escala, já que o registro/tabela fica travado e só
pode ser manipulado por um usuário. Além disso, o banco de dados utilizado precisa oferecer suporte ao
lock desejado (como select for update , por exemplo).
A segunda abordagem, o lock otimista, é a mais recomendada por não trazer problemas de
escalabilidade. O Hibernate parte do princípio de que não vai haver problema de concorrência algum,
portanto não faz nada para se precaver. SE ocorrer concorrência, o Hibernate apenas lança uma
exception para o segundo update, que estaria atropelando o update anterior.
Desta forma a aplicação pode tratar a exception lançada como desejar. Abordagens comuns são dizer
ao usuário para tentar novamente, ou mostrar na tela os dados da versão mais nova, para que ele mesmo
resolva os conflitos.
Fazer o Hibernate usar o lock otimista é muito simples. Basta haver um campo de versionamento na
entidade:
public class Conta {
// outros atributos...
@Version
private Integer versao;
Perceba o campo do tipo Integer anotado com @Version . A propriedade que guarda a versão
pode ser numérica ( Integer , Long ), ou um timestamp ( java.util.Date , java.util.Calendar ,
java.time.LocalDateTime ).
Caso o objeto já seja o mais atual, o Hibernate simplesmente incrementa a sua versão
automaticamente ao atualizá-lo.
// outros atributos...
@Version
private Integer versao; //getter e setter
// ...
}
2. Reinicie o WildFly e acesse o mysql para verificar o resultado. Deve ter sido criado uma coluna na
tabela Conta com o nome versao. Vamos deixar a versão das contas previamente criadas como zero.
Para isso, ainda no mysql, execute o seguinte comando:
3. Agora vamos simular dois usuários tentando alterar a mesma conta ao mesmo tempo.
Para isso, acesse o sistema, vá no cadastro de contas e clique para alterar alguma conta.
Com essa conta carregada, abra uma nova página, acesse o cadastro de contas e clique para alterar
a mesma conta.
Realize alguma alteração e clique no botão para confirmar a alteração.
Volte na primeira página e tente agora alterar a mesma conta carregada previamente, no caso com
uma versão anterior. Uma exception do tipo OptimisticLockException vai ser lançada
indicando que o mesmo objeto foi alterado em outra transação.
4. Volte o código do projeto pra como estava antes desse exercício, ou seja, retire o atributo criado na
classe Conta .
Poderíamos escrever uma query para buscar as contas, trazê-las para memória, iterar pela lista
alterando o atributo banco das instâncias e então comitar as alterações. O código seria algo como:
A questão é: Precisamos mesmo fazer um select e trazer objetos para a memória do nosso
servidor, quando o que realmente desejamos fazer é um update ? Além disso, o first level cache pode
ficar grande demais, conforme o número de contas que você carregar para atualizá-las. Precisaria fazer
um controle fino para limpar esse cache através do entityManager.clear() .
Uma alternativa é fazer a mesma operação diretamente no banco, alterando todas as contas sem
precisar trazê-las para a memória. Trata-se de uma operação em lote ( bulk operation ) e para
executar precisamos utilizar um update por jpql:
//entityManager já foi inicializado
String jpql = "UPDATE Conta c SET c.banco = :novoNome WHERE c.banco = :antigoNome";
A abordagem anterior possui uma desvantagem. Toda operação em lote é traduzida para SQL pelo
provider da JPA e executado diretamente contra o banco de dados, de forma que o container
desconhecerá totalmente essa operação: O cache de primeiro e segundo nível não perceberão qualquer
alteração!
return query.executeUpdate();
}
3. Na classe já tem dois atributos para o nome antigo e nome novo do banco. Vamos passá-los para o
método do DAO que acabamos de criar. Procure o método atualizar na classe
OperacaoEmLoteBean . Esse método é responsável por chamar o método criado no ContaDao :
this.contasAlteradas =
contaDao.trocaNomeDoBancoEmLote(antigoNomeBanco, novoNomeBanco);
Repare que o resultado fica guardado na variável contasAlteradas que a tela vai mostrar.
http://localhost:8080/fj25-financas-web/contas.xhtml
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Além disso, a integração com CDI melhorou. Podemos injetar qualquer dependência com CDI
dentro de EntityListeners
class ContaListener {
@Inject
private GeradorDeNumero gerador;
@PrePersist
public void geraNumero (Object o){
Conta conta = (Conta) o;
conta.setNumero(gerador.novoNumero());
}
}
CAPÍTULO 15
"Procure detalhes pequenos e sem importância para achar alguma falha." -- Provérbio holandês
Quando definimos o atributo que representa a chave primária com @Id , o banco de dados
automaticamente cuidará de sua unicidade garantindo que não haverá nenhum valor duplicado.
Há casos onde queremos definir outros atributos como únicos, não só a chave. Por exemplo, em
nossa classe Conta , pode fazer sentido declarar o atributo número também como único. A JPA possui
para isso a anotação @Column com qual podemos manipular a coluna associada com o atributo:
@Entity
//outras anotações
public class Conta {
@Id
@GeneratedValue
private Integer id;
@Column(unique=true)
private String numero;
@Column não serve apenas para configurar a unicidade do valor, mas também para definir o nome
da coluna, o tamanho do campo, não permitir alterações e não aceitar valores nulos:
@Column(name="description", length="20", nullable=false, updatable=false)
Como já aprendemos, qualquer tipo em Java é automaticamente mapeado para um tipo no banco.
Por exemplo, um String do mundo Java se torna um varchar(255) no MySQL. Através da anotação
@Column também é possível redefinir o tipo no banco:
@Column(columnDefinition="text")
private String descricao;
É preciso ter cuidado com essa configuração pois perdemos a portabilidade entre banco de dados
diferentes.
Em alguns casos, é preciso definir a unicidade baseada em várias colunas. Por exemplo, não faz
sentido ter apenas número como unique , mas o conjunto número e a agência. Para isso, o @Column
não serve já que é associado a apenas um atributo.
O JPA prevê a anotação @Table , que possui um atributo uniqueConstraints que recebe a
anotação @UniqueConstraint . Veja o exemplo:
@Entity
//outras anotações
@Table(uniqueConstraints= {@UniqueConstraint(columnNames={"agencia", "numero"})})
public class Conta {
@Table(uniqueConstraints= {
@UniqueConstraint(columnNames={"agencia", "numero"})
})
3. Tente também a anotação @Column : deixe a coluna para o nome do banco com um tamanho de 20
caracteres e não nulo:
@Column(length=20, nullable=false)
private String banco;
4. Reinicie o servidor WildFly. Depois, abra um terminal e se conecte com o MySQL. Verifique se a
coluna banco foi gerada não nula:
mysql -u root
use fj25;
desc Conta;
Tente inserir duas contas com o mesmo número e agência. A segunda conta deve causar uma
exceção.
No console do WildFly, procure a primeira exceção lançada. Repare que foi o MySQL que validou e
gerou a exceção.
6. Para que as tabelas sejam mantidas quando reiniciarmos o servidor, abra o persistence.xml e
retorne o valor da propriedade hbm2ddl para update.
O banco de dados sempre deve proteger-se e verificar a integridade dos dados antes de qualquer
alteração. No entanto, faz parte do trabalho do desenvolvedor verificá-los antes de enviar uma alteração
para o banco. Isso geralmente é resolvido com a criação de rotinas de validação de dados.
O desenvolvimento dessa validação de dados pode ser demorado e muitas vezes acaba se tornando
de difícil manutenção com o passar do tempo.
if (conta.getTitular() != null) {
entityManager.persist(conta);
}
No entanto, essa abordagem gera um problema, pois, sempre que precisarmos persistir uma Conta ,
devemos lembrar de fazer essa verificação em todos os lugares onde ela seja salva. Essa não é uma boa
abordagem. Estamos ferindo gravemente o princípio do encapsulamento que aprendemos quando
estudamos orientação a objetos, além de correr risco de esquecer de realizar essa validação.
Uma solução, então, seria adicionarmos essa validação dentro dos métodos adequados no nosso
DAO , dessa forma teríamos o comportamento encapsulado num único lugar e evitaríamos duplicidade
de código.
No entanto, sempre que tivermos essas validações corriqueiras precisaremos adicionar seu respectivo
código. Suponha que a entidade Conta possua 15 atributos que devem ser verificados antes de ser
persistida. Um if para cada um dos atributos? Se novos atributos fossem adicionados? Teríamos que
fazer diversos ifs , o que não é nada elegante.
Para isso, a especificação define um conjunto de anotações que podem ser colocadas em suas
entidades para que a validação seja realizada.
Por exemplo, para validarmos que o titular de uma Conta não será persistido com o valor nulo,
podemos utilizar a anotação @NotNull sobre o atributo titular :
@Entity
//outras anotações
public class Conta {
@NotNull
private String titular
Ao anotarmos o atributo titular , garantimos que ele não aceite valores nulos. A questão é:
quando a validação será executada e quem a realizará para nós?
O primeiro passo é conseguirmos uma fábrica de validadores. Para isso, utilizamos a classe
Validation invocando o método buildDefaultValidatorFactory() . Ele devolverá uma instância
de ValidatorFactory , que utilizamos para conseguir as instâncias dos validadores do Hibernate
Validator:
Com o uso da fábrica, conseguimos validadores invocando o método getValidator , que retorna
uma instância de Validator :
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Poderíamos também isolar a criação do ValidatorFactory em uma classe similar à JPAUtil que
criamos anteriormente:
No nosso caso, como estamos dentro de um container Java EE, não é preciso inicializar o Bean
Validation programaticamente. Novamente a inversão de controle aplica e ajuda o desenvolvedor. A
única coisa que precisamos fazer é injetar o Validator :
@Inject
private Validator validator;
Com o validador pronto, podemos criar um objeto do tipo Conta para tentarmos validá-lo. Ao
final, o objeto que queremos validar é passado como parâmetro para o método validate do
Validator . Esse método retorna um Set contendo os erros de validação, que são do tipo
ConstraintViolation . Dessa forma, podemos exibir os erros de validação iterando pelo Set
retornado, da seguinte forma:
Conta conta = new Conta(); //criamos uma conta sem titular preenchido
3. Na mesma classe ValidacaoBean , no método validar() , use o validador para receber os erros de
validação. O método geraMensagemJsf() já está preparado na classe ValidacaoBean .
Set<ConstraintViolation<Conta>> erros = validator.validate(conta);
<f:validateBean disabled="false">
6. (Opcional) Também valide o atributo banco da conta. Ele não pode ser nulo e deve ter no mínimo 3
e no máximo 20 caracteres. Use a anotação @Size .
@AssertTrue
@DecimalMax
@DecimalMin
@Digits
Indica quantos dígitos possuem a parte inteira e a parte decimal do valor indicado
@Future
@Max
@Min
@NotNull
@Null
@Past
@Pattern
@Size
@Valid
Há também anotações especificas do Hibernate Validator, que não vão funcionar em qualquer
implementação:
@Length
@NotEmpty
Verifica que o valor não é nulo ou não é uma String vazia, sem fazer o trim
Pertence à especificação: Não
@NotBlank
Verifica que o valor não é nulo ou não é uma String vazia, fazendo o trim
Pertence à especificação: Não
@Range
Quando anotamos com @NotNull , o titular da Conta ainda aceita valores em branco, ou seja, com
uma String vazia. Olhando a tabela dos validadores, notamos que existe a anotação @NotBlank , que
verifica se o valor está nulo e também se é uma String vazia. Dessa forma, podemos utilizá-la ao invés
da anotação @NotNull :
@Entity
public class Conta {
@NotBlank
private String titular
// outros atributos
}
No entanto, a anotação @NotBlank não faz parte da especificação Bean Validation. Ela é apenas
uma conveniência que existe no Hibernate Validator. Isso significa que, se um dia precisarmos mudar de
implementação, precisaremos alterar também a anotação do nosso modelo, algo que não precisamos nos
preocupar quando programamos voltado à especificação.
Como alternativa da especificação também podemos utilizar a anotação @Size para definir um
range de caracteres (min e max), por exemplo:
Uma outra validação interessante que podemos adicionar ao nosso modelo é a Movimentacao só
poder ter valores positivos. Podemos conseguir isso através da anotação @DecimalMin , passando uma
String contendo o valor mínimo aceitável para o valor.
@Entity
public class Movimentacao {
@Id
@GeneratedValue
private Integer id;
@DecimalMin("0.01")
private BigDecimal valor;
// outros atributos
}
Todas as anotações de validação recebem um atributo chamado message , no qual podemos passar
um valor contendo a mensagem que queremos que seja exibida. Dessa forma, poderíamos customizar a
mensagem de erro para a validação do valor mínimo da movimentação da seguinte forma:
@DecimalMin(value="0.01", message="O valor deve ser igual ou acima de 1 centavo")
private BigDecimal valor;
Agora, quando a validação falhar, a mensagem que definimos na anotação será a utilizada para
descrever o erro. O ponto negativo dessa abordagem é que sempre que utilizarmos essa validação
precisaremos sobrescrever a mensagem dentro da anotação, com isso a String ficará espalhada por
toda a nossa aplicação.
Podemos melhorar isolando as mensagens em um lugar único que o Hibernate Validator suporte e
encontre essas mensagens. Esse lugar é o arquivo ValidationMessages.properties que deve ficar
dentro do diretório src do seu projeto.
Dentro desse arquivo podemos sobrescrever as mensagens padrões do Hibernate Validator. Por
exemplo, podemos mudar a mensagem da validação do @DecimalMin , simplesmente adicionando a
seguinte linha no nosso novo properties :
javax.validation.constraints.DecimalMin.message = Deve ser maior ou igual a {value}
Do lado esquerdo adicionamos uma chave que indica qual é a mensagem que queremos alterar
enquanto que à direita informamos qual é o valor que desejamos alterar. No caso, usamos o valor do
parâmetro value dinamicamente na mensagem.
Caso seja necessário conhecer todas as chaves possíveis para alterar as mensagens de erro de
validação, o arquivo contendo as mensagens padrões do Hibernate Validator pode ser encontrado
dentro do seu .jar no pacote org.hibernate.validator .
A princípio, nossa regra de negócio diz que não é preciso informar a agência e o número da Conta ,
no entanto, quando uma delas for preenchida, ambas deverão ser informadas.
Não conseguimos fazer isso anotando os nossos atributos. No entanto, queremos utilizar o
Hibernate Validator para disparar a nossa validação. O quê podemos fazer a respeito?
Nesse caso, poderíamos criar o nosso próprio validador que o Hibernate Validator entende e
consegue executar para nós quando pedimos para ele executar as validações.
Criar um validador customizado demanda alguns passos. O primeiro é definirmos a nossa própria
anotação que marcará a classe indicando que ela sofrerá uma validação. No nosso caso, chamaremos a
nossa anotação de @NumeroEAgencia . Ao criarmos a anotação, precisamos indicar que ela será retida
em tempo de execução e que a anotação será colocada em classes:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NumeroEAgencia {
A anotação deve ainda declarar os atributos message , groups e payload . O atributo message
define a chave da mensagem que será utilizada para buscar uma no arquivo de mensagens do Hibernate
Validator. Já, o atributo groups define o nome do agrupamento para essa validação que o Hibernate
Validator gerenciará internamente. Por fim, o atributo payload , que é requerido pela especificação
mas o framework a ignora, serve para estender a validação com objetos customizados. Dessa forma
teremos:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NumeroEAgencia {
Por fim, precisamos anotar a nossa interface com a anotação @Constraint indicando em qual
classe estará o código que executará a validação. Podemos declarar a classe
NumeroEAgenciaValidator . Adicionando a anotação, teremos o seguinte código:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NumeroEAgenciaValidator.class)
public @interface NumeroEAgencia {
A interface ConstraintValidator declara dois métodos que precisamos implementar, que são o
initialize e o isValid .
O método initialize recebe como parâmetro a anotação que declaramos no objeto para ser
validado. Assim podemos recuperar algum parâmetro que especificamos na anotação. Já o método
isValid é onde adicionaremos a nossa validação e retornaremos um booleano indicando se a
validação foi feita com sucesso ou não.
Agora, precisamos implementar o método isValid . Vamos utilizar a Conta que é recebida como
parâmetro e caso ela seja nula, retornaremos true , pois esse não é o objetivo da nossa validação.
Após essa verificação, precisamos verificar se o valor da agencia e do numero da Conta estão
informados. Caso estejam, precisamos retornar true . Conseguimos fazer isso simplesmente através do
uso do operador XOR (ou exclusivo). Dessa forma, teremos a seguinte implementação para o método
isValid :
if (conta == null) {
return true;
}
return ! (conta.getAgencia() == null ^ conta.getNumero() == null);
@NumeroEAgencia
@Entity
public class Conta {
// atributos e métodos
}
@Override
public void initialize(NumeroEAgencia anotacao) {
}
@Override
public boolean isValid(Conta conta, ConstraintValidatorContext ctx) {
if(conta == null) {
return true;
}
boolean agenciaEhVazia =
(conta.getAgencia() == null ||
conta.getAgencia().trim().isEmpty());
boolean numeroEhVazio =
(conta.getNumero() == null ||
conta.getNumero().trim().isEmpty());
4. Anote a classe Conta para informar que ela deve ser validada baseada em nossa nova anotação.
Basta usar a nova anotação @NumeroEAgencia no topo da classe.
Cada validação deve ser especializada seguindo o princípio DRY ( Don't Repeat Yourself ). Para
aproveitarmos a implementação de anotações já existentes, podemos agrupar várias anotações de
validação em outra anotação de validação. A anotação abaixo valida se um campo String é não nulo,
possui no mínimo 5 caracteres dos quais o primeiro é uma letra maiúscula. Se um campo for anotado
com ela e qualquer uma das validações falhar ocorrerá um ou mais ConstraintViolation , com as
respectivas mensagens das anotações originais.
@NotNull
@Length(min=5)
@Pattern(regexp="[A-Z].*")
@Constraint(validatedBy={})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MaiusculaNaoNulaELonga {
Class<?>[] groups() default {};
15.12 PARA SABER MAIS: AGRUPANDO ANOTAÇÕES DE VALIDAÇÃO PARA EVITAR REPETIÇÃO 203
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
Para que a validação das associações seja feita, precisamos anotar as associações com a anotação
@Valid . Nesse caso, se quiséssemos validar a Conta ao salvar uma movimentação, poderíamos anotar
o atributo conta .
@Entity
public class Movimentacao {
@Id
@GeneratedValue
private Integer id;
@Valid
@ManyToOne
private Conta conta;
// outros atributos
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
//...
}
}
Outro ponto interessante é a integração do Bean Validation com CDI. Assim podemos aproveitar a
injeção de dependência com CDI nos _Validator_s do Bean Validation:
public class NumeroEAgenciaValidator
implements ConstraintValidator<NumeroEAgencia, Conta> {
@Inject
private ContaDao contaDao;
//método omitidos
}
Além do suporte dos frameworks, tem crescido também a disponibilidade de validadores criados
pela comunidade compatíveis com Bean Validation. É o caso do Caelum Stella, que permite que
validemos nossas entidades usando @CPF e @CNPJ, por exemplo.
No curso FJ-26, onde focamos o uso do JSF 2, vemos com mais detalhes as maneiras de lidar com
validação usando esse framework MVC.
Outra integração importante que conseguimos fazer é com a JPA; dessa forma, modelos inválidos
não serão persistidos através dos métodos de persistência como persist ou merge .
Ao tentar persistir
um objeto cuja validação falhe, é lançada uma
ConstraintValidationException . Por meio desta exception, conseguimos recuperar a mesma lista de
erros que usamos nos exercícios iniciais. O método que precisamos invocar é
getConstraintViolations() , mas o ideal é que esses dados já tivessem sido validados antes.
Essa integração já vem habilitada por padrão. E para desabilitá-la, basta adicionar a seguinte linha no
persistence.xml :
CAPÍTULO 16
MAIS MAPEAMENTOS
"Quando se diz às pessoas que a felicidade é uma questão simples, querem-nos sempre mal." -- Bertrand
Russel
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
Precisamos agora dizer que a conta será cuidada por um gerente. Para isso, vamos adicionar o
atributo gerente na classe Conta e indicarmos a cardinalidade do relacionamento, que nesse caso
será @OneToOne :
@Entity
public class Conta {
// outros atributos
@OneToOne
private Gerente gerente;
Com isso, o Hibernate vai criar um campo gerente_id na tabela Conta referenciando a tabela
Gerente .
Mas existe um problema, para que o gerente só possa cuidar de uma única Conta é preciso que o
atributo gerente_id na tabela Conta seja único, algo que a anotação @OneToOne não nos fornece
por padrão. Para conseguirmos esse comportamento precisamos anotar a classe Conta com @Table e
indicar no atributo uniqueConstraints que queremos que o campo gerente_id tenha uma chave
única:
@Entity
@Table(uniqueConstraints= {@UniqueConstraint(columnNames="gerente_id")})
public class Conta {
// resto do código
}
Essa configuração também pode ser feita por meio da anotação @JoinColumn :
@Entity
//@Table(uniqueConstraints= {@UniqueConstraint(columnNames="gerente_id")})
public class Conta {
@OneToOne
@JoinColumn(unique=true)
private Gerente gerente;
// restante do código
}
@Entity
public class Gerente {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
// getters e setters
}
// outros atributos
@OneToOne
private Gerente gerente;
// getters e setters
}
Como o Hibernate não deixa o relacionamento @OneToOne como único, vamos criar uma
restrição unique através da anotação @JoinColumn :
@Entity
public class Conta {
// outros atributos
@OneToOne
@JoinColumn(unique=true)
private Gerente gerente;
// restante do código
Mas nem todo objeto funciona dessa maneira, existem objetos que neste ponto tem um
comportamento contrário, ao invés de buscar pela sua identidade, a busca é feita pelo seu valor. "Quero
um objeto que represente a cor azul!". Qualquer objeto que o valor represente a cor azul caberá na busca,
pois o que importa é seu valor. Este tipo de objeto que apenas serve para representar um ou mais valores
relativos ao domínio é chamado de Value Object.
Como a função dos Value Objects é apenas representar valores, dois objetos com as mesmas
informações, são mutualmente intercambiáveis, onde serve um, serve o outro. Para o sistema os dois são
o mesmo, eles não precisam ser unicamente identificáveis, por isso eles não precisam implementar um
sistema de identificação. Isso facilita o design dessas classes.
A falta de identidade e a possibilidade de troca de objetos traz outra vantagem em relação ao uso de
memória, os objetos do tipo Value Object são facilmente instanciados. Ao surgir a necessidade daquele
valor basta criar o objeto, porém eles também são descartáveis, quando ele não for mais necessário e
perder a referência, o garbage collector se encarregará dele, isso não atrapalha o ciclo de vida dele, afinal
ele não tem.
Como não precisamos de uma nova tabela e muito menos de relacionamento, essa classe Endereco
não deve ter um id , e sim apenas as propriedades:
public class Endereco {
// getters e setters
}
Por fim, precisamos dizer que a classe Endereco não é uma nova Entidade e sim é um objeto que
poderá ser "embutido" em outros objetos. Para isso, anotamos a classe Endereco com @Embeddable :
@Embeddable
public class Endereco {
// getters e setters
}
Agora podemos remover os 3 atributos que representam o endereço da classe Gerente e substituí-
la por Endereco , juntamente com a anotação @Embedded :
public class Gerente {
// outros atributos
@Embedded
private Endereco endereco;
// getters e setters
}
A criação de Embeddables é um dos recursos interessantes para atingirmos uma solução bem
orientada a objeto, sem que a modelagem do banco de dados sofra com a modelagem de nossas
classes. Confira mais detalhes no post: http://blog.caelum.com.br/adequar-o-banco-as-entidades-
ou-o-contrario/
@Embeddable
public class Endereco {
// getters e setters
}
2. Na classe Gerente remova os três atributos referentes ao endereço e troque por um atributo da
classe Endereco :
@Entity
public class Gerente {
// outros atributos
@Embedded
private Endereco endereco = new Endereco();
// getters e setters
}
3. Vamos criar uma nova classe DAO de gerentes para encapsular o acesso aos dados de Gerente. Para
isso, crie a classe GerenteDao no pacote br.com.caelum.financas.dao .
@Stateless
public class GerenteDao {
@Inject
private EntityManager manager;
``` java
@Stateless
public class GerenteDao {
@Inject
private EntityManager manager;
1. A classe GerentesBean será utilizada pelo JSF para a tela de adição e listagem de novos gerentes.
Vamos implementar as funcionalidades para que a tela funcione corretamente:
@Inject
private GerenteDao gerenteDao = new GerenteDao();
Crie o método getGerentes() para recuperarmos a lista de gerentes cadastrados no banco que
será exibida na tela.
public List<Gerente> getGerentes() {
if(this.gerentes == null){
this.gerentes = gerenteDao.lista();
}
return gerentes;
}
Para finalizar, crie os métodos grava e remove conforme fizemos anteriormente para os outros
managedBeans .
2. Agora precisamos fazer com que as novas contas adicionadas possam ser associadas a um gerente
existente. Vamos habilitar os campos na tela de contas para que essa associação seja possível:
Altere o valor a propriedade rendered para true em ambas as tags. O código ficará assim:
<h:outputText value="Gerente" rendered="true"/>
<h:selectOneMenu id="gerente" value="#{contasBean.gerenteId}" rendered="true">
A lista de contas também precisa exibir o gerente de cada uma das contas, caso ele exista. Para isso
vamos habilitar a exibição dessa coluna. Procure pelo seguinte trecho de código:
<h:column rendered="false">
<f:facet name="header"><h:outputText value="Gerente"/></f:facet>
#{conta.gerente.nome}
</h:column>
<h:column rendered="true">
<f:facet name="header"><h:outputText value="Gerente"/></f:facet>
#{conta.gerente.nome}
</h:column>
3. Para finalizar, vamos alterar a classe ContasBean para que ela associe o gerente selecionado na tela
à nova conta antes de adicioná-la ao banco de dados.
@Inject
private GerenteDao gerenteDao;
Altere o método grava para, caso um gerente tenha sido escolhido na tela, buscá-lo no banco de
dados através do atributo gerenteId e associá-lo à nova conta.
public void grava() {
if(gerenteId != null){
Gerente gerenteRelacionado = gerenteDao.busca(gerenteId);
this.conta.setGerente(gerenteRelacionado);
}
//restante do código existente omitido ...
}
@Embedded
@AttributeOverrides({
@AttributeOverride(name="rua", column=@Column(name="rua_alternativa")),
@AttributeOverride(name="cidade", column=@Column(name="cidade_alternativa"))
@AttributeOverride(name="estado", column=@Column(name="estado_alternativo"))
})
private Endereco enderecoAlternativo = new Endereco();
//...
}
A abordagem acima pode causar transtorno quando a quantidade de atributos Embbeded na mesma
classe for muito grande ou a quantidade de atributos do Embeddable for grande. Outra opção para
diferenciar o nome das colunas do Embeddable é customizar a estratégia de nomenclatura usada pelo
Hibernate. Para isso podemos colocar mais uma configuração no persistence:
<property name="hibernate.ejb.naming_strategy"
value="org.hibernate.cfg.DefaultComponentSafeNamingStrategy"/>
Por exemplo, queremos que as chaves primarias da entidade Gerente sejam o RG e o CPF dele,
então criamos a classe GerenteId e usamos como id:
@Embeddable
public class GerenteId implements Serializable {
// gets e sets
}
@Entity
public class Gerente {
@EmbeddedId
private GerenteId id;
// ...
}
Muita gente reclama do fato dos atributos da chave estar numa segunda classe, separada da
entidade. Há para isso uma terceira opção. Cria-se a segunda classe com os atributos, mas depois
copia-se todos os atributos para a entidade. Na entidade, usa-se a anotação @IdClass para indicar
qual é a classe de id.
@Entity @IdClass(GerenteId.class)
public class Gerente {
@Id
private String rg;
@Id
private String cpf;
CAPÍTULO 17
Muitas pessoas utilizam alguma ferramenta de engenharia reversa para gerar o modelo de objetos
com as devidas anotações a partir de um banco de dados existente. Uma ferramenta bastante conhecida
e que possui essa funcionalidade é o JBoss Tools.
No entanto, mesmo que a ferramenta gere o mapeamento das entidades para nós, é importante
compreendermos o que cada anotação faz. Mesmo tendo visto diversos mapeamentos diferentes durante
o curso, acaba se tornando inviável abordarmos todas as existentes, portanto recomendamos fortemente
ao aluno que leia a documentação do Hibernate Annotations disponível em:
http://docs.jboss.org/hibernate/stable/annotations/reference/en/pdf/hibernate_referen
ce.pdf
É importante ter em mente que o Hibernate possui diversos pontos de extensão, que nos permite
alterar também vários dos seus comportamentos padrão com relação ao banco de dados. Um desses
pontos de extensão comuns que existe é o uso do Hibernate Envers para fazer auditoria dos dados.
Single table
Table per Class
Joined
Cada modo tem suas vantagens e desvantagens. Vamos analisar cada modo separadamente. Imagine
que agora teremos dois tipos de gerentes no nosso banco, um para gerenciar alguma conta do banco, e
outro para gerenciar equipes internas e seus projetos. Teríamos o seguinte modelo:
class Gerente{
//atributos omitidos
}
Single table
A opção Single Table criará uma única tabela para todas as entidades com todas as colunas
disponíveis em todas as entidades, além de uma a mais chamada DTYPE, que identificará o registro pelo
Tabela Gerente
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| ID | int(11) | NO | PRI | NULL | |
| DTYPE | varchar(31) | YES | | NULL | |
| cidade | varchar(255) | YES | | NULL | |
| estado | varchar(255) | YES | | NULL | |
| rua | varchar(255) | YES | | NULL | |
| nome | varchar(255) | YES | | NULL | |
| telefone | varchar(255) | YES | | NULL | |
| nomeDaEquipe | varchar(255) | YES | | NULL | |
| numeroDaConta | varchar(255) | YES | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
Dentre todas as estratégias disponíveis para mapear heranças, esta é a mais performática, visto que
precisamos acessar apenas uma única tabela ao trabalhar com quaisquer das subclasses envolvidas.
Porém não é possível realizarmos validações no banco de dados através de constraints , pois todas as
colunas de diferentes modelos estão presentes na mesma tabela.
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
public class Gerente {
//...
}
@Entity
public class GerenteConta extends Gerente{
//...
}
@Entity
public class GerenteEquipe extends Gerente{
//...
}
Tabela GerenteConta
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| cidade | varchar(255) | YES | | NULL | |
| estado | varchar(255) | YES | | NULL | |
| rua | varchar(255) | YES | | NULL | |
| nome | varchar(255) | YES | | NULL | |
| telefone | varchar(255) | YES | | NULL | |
| numeroDaConta | varchar(255) | YES | | NULL | |
+---------------+--------------+------+-----+---------+-------+
Tabela GerenteEquipe
+--------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| cidade | varchar(255) | YES | | NULL | |
| estado | varchar(255) | YES | | NULL | |
| rua | varchar(255) | YES | | NULL | |
| nome | varchar(255) | YES | | NULL | |
| telefone | varchar(255) | YES | | NULL | |
| nomeDaEquipe | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+-------+
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Gerente {
//...
}
Joined
Nessa estratégia a JPA trabalhará com tabelas com o mínimo de informações possíveis. Cada classe
terá uma tabela apenas com seus atributos, e as tabelas das classes filhas terão uma coluna a mais que
será uma chave estrangeira para a tabela pai.
Quando salvarmos um objeto de uma das classes filhas a JPA vai gerar um registro na tabela filha e
um na tabela pai.
Tabela Gerente
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| cidade | varchar(255) | YES | | NULL | |
| estado | varchar(255) | YES | | NULL | |
Tabela GerenteConta
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| numeroDaConta | varchar(255) | YES | | NULL | |
+---------------+--------------+------+-----+---------+-------+
Tabela GerenteEquipe
+--------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| nomeDaEquipe | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+-------+
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class Gerente {
//...
}
Uma das vantagens do Hibernate, e que pode salvar bastante tempo do DBA com a criação do banco
de dados é que ele pode gerar também os scripts de criação das tabelas, também conhecidos como DDL.
Dessa forma, nós podemos enviar esse script para que o próprio DBA faça a criação para nós.
Muita gente também não gosta de poluir suas classes de modelo com informações específicas do
schema do banco, irrelevantes para o mapeamento objeto-relacional, como, por exemplo, os índices
( @Index ), constraints ( @UniqueConstraint ) e até mesmo detalhes sobre tamanho dos campos
( @Column(lenght=100) ). Todos esses exemplos são importantes para o schema relacional mas não
importantes para o programa e seu mapeamento. Uma abordagem que pode ser feita é adicionar essas
informações diretamente ao schema do banco sem interferir na classe da entidade.
Utilizando a JPA via Hibernate, podemos criar uma classe com um método main e utilizarmos a
classe SchemaExport , específica do Hibernate, para fazer a criação do banco de dados para nós.
Precisamos passar para ela as configurações do projeto, mas como usamos JPA e configurações da JPA,
precisamos convertê-las para o Hibernate usando a classe EJB3Configuration :
O método create do SchemaExport recebe dois booleanos, sendo que o primeiro indica que
queremos visualizar o DDL no console e o segundo indica se queremos executar o DDL. Nesse caso, só
queremos visualizar o script sem executá-lo.
O SchemaExport vai gerar todas as tabelas do banco de dados, adicionando um comando para
excluir as tabelas existentes. Caso a ideia seja apenas fazer alterações como adicionar colunas novas na
tabela, pode se usar a classe SchemaUpdate ao invés de SchemaExport .
Crie a classe GerenteConta e faça-a herdar da classe Gerente . Ela também possuirá um
atributo do tipo String chamado numeroDaConta, que armazenará o numero da conta
relacionada ao gerente.
Para indicar que a nova classe deve ser gerenciada pela JPA, utilize a anotação @Entity sobre a
mesma.
@Entity
public class GerenteConta extends Gerente{
//métodos omitidos
}
3. Para que a nova classe GerenteConta seja utilizada, vamos alterar o nosso código responsável por
adicionar novos gerentes:
Abra a classe GerentesBean e altere o atributo gerente para que referencie a um novo
GerenteConta .
4. Para finalizar, vamos adicionar o número da conta relacionada ao gerente em seu atributo
numeroDaConta antes de gravar uma nova conta no banco de dados:
O primeiro passo é alterar o método busca na classe GerenteDao para que retorne um objeto
do tipo GerenteConta :
public GerenteConta busca(Integer id) {
return manager.find(GerenteConta.class, id);
}
Agora que o método busca retorna um GerenteConta , podemos alterar o método grava da
classe ContasBean para associar o número da conta ao gerente:
if(gerenteId != null){
GerenteConta gerenteRelacionado = gerenteDao.busca(gerenteId);
gerenteRelacionado.setNumeroDaConta(this.conta.getNumero());
this.conta.setGerente(gerenteRelacionado);
}
6. Vamos analisar como estão organizadas as tabelas no banco de dados e como o gerente e a conta que
e depois
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
CAPÍTULO 18
Imagine que queremos dar a possibilidade para o usuário de realizar uma pesquisa podendo
selecionar uma conta ou não. Para não ficar concatenando strings diretamente, vamos usar um
StringBuilder para realizar essas operações. Então ficamos com um código parecido com:
Agora, precisamos testar se foi passada ou não a conta. Então, podemos adicionar um if :
Então já temos que lembrar de dar esse espaço. Outro problema é que, temos de alguma forma
guardar os parâmetros que foram utilizados na JPQL para depois substituí-los para a execução da query.
O usuário também pode escolher o tipo de movimentação que ele quer trazer, de saída ou de entrada.
Então vamos adicionar mais uma condição:
Tudo parece certo? O problema aqui é que talvez uma conta não tenha sido passada e, como não
entramos no primeiro if , também não colocamos o where . Como consequência, na hora da execução
da query, vamos receber um erro de sintaxe.
Podemos até gastar um tempo para ir arrumando cada problema, mas vamos gastar muito tempo em
uma tarefa que não é o foco. O que queremos na verdade é simplesmente montar uma consulta
dinamicamente. A JPA2 oferece um conjunto de classes que já resolveram este problema para gente e
permite montarmos query orientada a objetos. Vamos começar a estudar a Criteria API.
CriteriaBuilder
O CriteriaBuilder é uma fábrica auxiliar para criar expressões sobre as funções que utilizaremos
na busca. A fábrica não executa a query em si, ela apenas ajuda a criar. Ela é muito parecida com as
classes Restrictions ou Projections da API de Criteria do Hibernate.
O CriteriaBuilder possui métodos que definem operações da busca como por exemplo:
Qualquer método acima retorna um objeto do tipo Predicate ou Expression , ambos são
interfaces ( Predicate estende a interface Expression ). Por exemplo, podemos definir um filtro
usando um método do builder:
Predicate lesser = builder.lesserThan(caminho, 500.5 )
Para determinar o que estamos filtrando ou somando existe o MetaModel (no exemplo indicado
pelo caminho) que veremos um pouco mais na frente. Primeiro, é preciso conhecer a definição da query,
a CriteriaQuery.
CriteriaQuery
CriteriaQuery
é a definição da pesquisa. O método createQuery(classe) recebe como
parâmetro o tipo do resultado da query. Se buscarmos Movimentações devemos colocar
Movimentacao.class , ou se quisermos somar o valor de todas as movimentações
BigDecimal.class . O CriteriaBuilder também é responsável pela criação da CriteriaQuery :
Imagine que queiramos apenas os valores de cada movimentação, nesse caso, precisamos definir que
o tipo de retorno da consulta agora é um BigDecimal . O exemplo alterado fica da seguinte forma:
CriteriaQuery<BigDecimal> criteria = builder.createQuery(BigDecimal.class);
Perceba que, agora, criamos uma criteria de busca informando que o tipo de retorno é BigDecimal .
Se tentarmos criar uma query baseado apenas nesta definição, do jeito que está, vamos receber uma
exception informando que ele não sabe de onde deve ser feita a consulta.
Temos que, explicitamente, informar de onde deve ser feito o select . Existe um método no
CriteriaQuery cujo o nome é from e, através deste método, podemos passar a classe que vai servir de
base na consulta. Nosso código está da seguinte forma:
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Movimentacao> criteria = builder.createQuery(Movimentacao.class);
criteria.from(Movimentacao.class);
Com a CriteriaQuery descrevemos a busca parecido com o JPQL. Quando escrevermos JPQL,
usaremos palavras chaves como select , from , where ou groupBy . As mesmas palavras chaves
aparecem na CriteriaQuery , mas aqui são nomes de métodos. Resumindo, encontramos os seguintes
select
from
where
orderBy
groupBy
having
Os métodos trabalham com Expression ou Predicate . O método select , por exemplo, recebe
como parâmetro uma Expression . Então podemos usar o CriteriaBuilder para criar uma
Expression :
Para definir um filtro usaremos o método where que recebe 0, 1 ou mais Predicate s:
Predicate lesser = builder.lesserThan(caminho, 500.5);
criteria.where(lesser);
A próxima imagem seguinte mostra a execução de uma CriteriaQuery comparando com JPQL:
MetaModel
No exemplo anterior vimos como pegar a Root:
//a movimentação como raiz
Root<Movimentacao> root = criteria.from(Movimentacao.class);
O método from retorna um objeto que representa a mesma ideia do alias que usamos quando
estamos criando uma consulta usando a JPQL. O tipo deste objeto retornado é Root .
A raiz é usada para acessar o MetaModel e definir os caminhos a partir dela. Por meio desse model
descrevemos o que queremos filtrar, somar ou calcular. Por exemplo, se quisermos pesquisar pelo valor
da movimentação usaremos:
Path é uma interface que estende Expression . Root por sua vez também é um Path . O mesmo
filtro pode ser escrito mais compactado sem variáveis auxiliares:
criteria.where(
builder.lesserThan(
root.<BigDecimal>get("valor"),
new BigDecimal("500.0"))
);
Com o Root na mão podemos também determinar o retorno da query. Aqui entra o método
select da query (opcional). Veja o código seguinte:
O JoinType é opcional.
Mas claro que queremos usar a Criteria. Para isso, crie na classe MovimentacaoDao um novo
4. Com o critério mais simples criado, podemos agora pedir para o EntityManager criar uma query
( TypedQuery ) baseada nesse critério e executar com o método getResultList .
@Inject
private MovimentacaoDao movimentacaoDao;
Desta vez teremos que usar os métodos select , from e where da CriteriaQuery. Também
trabalharemos com o MetaModel para definir o filtro ( m.conta.titular ) e a projeção
( m.valor ). O CriteriaBuilder auxilia na definição da query para fazer um like(..) e sum(..) .
A query vai retornar a soma das movimentações que vai ser do tipo BigDecimal . Vamos usar
este tipo na criação da query. Dentro do método somaMovimentacoesDoTitular escreva:
CriteriaBuilder builder = this.manager.getCriteriaBuilder();
CriteriaQuery<BigDecimal> criteria = builder.createQuery(BigDecimal.class);
return this.manager.createQuery(criteria).getSingleResult();
this.soma = movimentacaoDao.somaMovimentacoesDoTitular(this.titular);
web/criteria.xhtml .
Adicionamos mais um método na classe MovimentacaoDao para realizar esta busca baseada em
filtros dinâmicos. Inicialmente, criaremos um CriteriaQuery informando o tipo de resultado da nossa
busca. Precisamos também verificar as condições para ir montando a consulta. Para poder testar, por
exemplo, se existe uma movimentação igual à conta passada como parâmetro, temos que recuperar esse
atributo dentro da consulta. Para isso, mais uma vez, vamos recuperar um objeto do tipo Root . Temos
o seguinte código inicial:
public List<Movimentacao> pesquisa(Conta conta,
TipoMovimentacao tipo) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<Movimentacao> criteria =
builder.createQuery(Movimentacao.class);
Root<Movimentacao> root = criteria.from(Movimentacao.class);
}
Para nosso caso, vamos querer adicionar um AND na nossa consulta cada vez que precisarmos uma
nova condição no filtro. Para poder adicionar quantas condições forem necessárias vamos fazer uso de
um objeto do tipo Predicate . Para recuperar um objeto deste tipo, que consiga adicionar AND a cada
nova condição, invocamos o método da classe CriteriaBuilder chamado conjunction . Com isso
temos o seguinte código:
public List<Movimentacao> pesquisa(Conta conta,
TipoMovimentacao tipo) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<Movimentacao> criteria =
builder.createQuery(Movimentacao.class);
Root<Movimentacao> root = criteria.from(Movimentacao.class);
Predicate conjunction = builder.conjunction();
}
Repare que a classe CriteriaBuilder possui métodos que podemos utilizar para montar a nossa
query. Por exemplo, adicionamos um AND para o nosso objeto Predicate cuja condição que deve ser
testada é se a conta da movimentação é igual a conta que foi passada como parâmetro. Agora, depois de
termos definido todas as condições necessárias para a nossa pesquisa, vamos, de fato, adicioná-la a nossa
busca. Para isso vamos invocar o método where da classe CriteriaQuery . Com isso temos o seguinte
código:
public List<Movimentacao> pesquisa(Conta conta,
TipoMovimentacao tipo) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<Movimentacao> criteria =
builder.createQuery(Movimentacao.class);
Root<Movimentacao> root = criteria.from(Movimentacao.class);
Predicate conjunction = builder.conjunction();
if (conta != null) {
conjunction = builder.and(conjunction,
builder.equal(root.<Conta>get("conta"), conta));
}
if (tipo != null) {
conjunction = builder.and(conjunction,
builder.equal(root.<TipoMovimentacao>get("tipo"), tipo));
}
criteria.where(conjunction);
return this.em.createQuery(criteria).getResultList();
}
Tentando deixar a consulta, utilizando a Criteria API, menos propensa a erros, a JPA 2 introduziu
uma maneira de nos referenciarmos aos atributos do nosso modelo ainda mais orientada a objetos. Em
vez de passarmos a String para o método get , podemos passar um objeto que representa esse
atributo. Primeiro precisamos, de alguma maneira, recuperar um objeto que contém todas as
informações referentes à nossa classe mapeada. O EntityManager tem um método chamado
getMetaModel . Este método retorna um objeto que consegue recuperar informações sobre quaisquer
entidades mapeadas. Então temos o seguinte:
EntityManager em = //recuperar EntityManager;
Metamodel metamodel = em.getMetamodel();
Com um objeto do tipo MetaModel criado, podemos pedir informações sobre qualquer entidade.
Para isso usamos o método entity passando como parâmetro a classe que queremos pegar
informações. Este método nos retorna um objeto do tipo EntityType . Então, agora teríamos o seguinte
código:
Um detalhe interessante é que, ao contrário da convenção do Java, demos o nome à nossa variável de
movimentacao_. Essa foi a convenção adotada para a JPA 2 quando temos uma variável que contém um
objeto do tipo EntityType , ou seja, nomeDaClasse_ . Como estamos trabalhando com a
Movimentacao , ficou movimentacao_. Imagine agora que queremos acessar o atributo valor da
Movimentacao , para isso invocamos o método getSingularAttribute . Ele nos retorna um objeto
que representa nosso atributo, e podemos agora usar o método get da classe Root passando ele como
parâmetro. Temos então o seguinte código:
Repare que agora temos um objeto que representa nosso atributo, porém tivemos que escrever muito
mais código. Visando diminuir o código para usar a Criteria API com o Meta Model, a JPA 2 trouxe uma
outra maneira de recuperarmos os objetos que representam nossos atributos.
O problema é dessa abordagem era a de ter que ficar escrevendo uma classe semelhante para toda
entidade gerenciada do nosso projeto. Mas perceba que a ideia é exatamente a mesma que usamos
anteriormente, ter uma maneira typesafe de nos referenciarmos aos atributos da nossa entidade.
Percebendo isso, a JPA 2 trouxe uma maneira de geramos essas classes automaticamente, mas em vez de
todos atributos dela serem do tipo String , são dos tipos definidos pela JPA 2, como o
SingularAttribute por exemplo. Então a classe gerada, para a Movimentacao , teria o seguinte
código:
@StaticMetamodel(Movimentacao.class)
public abstract class Movimentacao_ {
Agora, o mesmo código que fizemos anteriormente, criando Metamodel dinamicamente, ficaria
assim:
Root<Movimentacao> root = criteria.from(Movimentacao.class);
root.get(Movimentacao_.valor);
Na JPA 2, essa classe é chamada de Static Metamodel. A classe responsável pela geração do Static
Metamodel vai variar de provider para provider. As configurações que faremos durante o treinamento
serão realizadas pelo Hibernate. Para outro provider, basta trocar o JAR da implementação. Para fazer
essa geração automática, temos que adicionar algumas opções para o javac no momento da compilação
dos nossos arquivos:
Dessa maneira, quando compilarmos o código, automaticamente serão geradas as classes contendo
informações sobre os atributos de cada entidade mapeada. Normalmente isso fica configurado na IDE
utilizada para desenvolvimento, para que não percamos tempo gerando o Static Metamodel. Veremos
também como fazer pelo Eclipse.
if (tipoMovimentacao != null) {
conjunction = builder.and(conjunction,
builder.equal(
root.<TipoMovimentacao>get("tipoMovimentacao"),
tipoMovimentacao));
}
criteria.where(conjunction);
return manager.createQuery(criteria).getResultList();
}
Note que, para adicionar o filtro por mês, precisamos usar a mesma função month que usamos com
a JPQL. Para acessar funções SQL através da Criteria API usamos o método function passando o
tipo de retorno e o atributo que deve ser utilizado para recuperar o valor e passar para a função.
Evite este problema e tente que, ao buscar as movimentações, sejam carregadas as contas
automaticamente. Dica: Utilize o objeto Root<Movimentacao> movimentacao juntamente com o
método fetch .
2. Tente efetuar uma busca nas movimentações filtrando pelo nome do titular da conta. Dica: Utilize o
objeto Root<Movimentacao> movimentacao juntamente com o método join .
1. Tente alterar o projeto, para ao invés de usar a abordagem com strings, usar o Static Metamodel.
CAPÍTULO 19
"A paz é mais importante que qualquer porção de terra" -- Anwar Sadat
No entanto, ambas as alternativas possuem um forte ponto negativo: são formas muito trabalhosas.
Uma terceira alternativa seria utilizar o próprio Hibernate e utilizar seu recurso de listerners que
nada mais são do que classes que podem ser registradas para executar algum código quando uma
operação é disparada através do Hibernate. Dessa forma "apenas" teríamos que desenvolver tais listeners.
Mas e como definir o que auditar? O que não auditar? E a codificação desses listeners para satisfazer
todas as regras? Temos uma solução melhor que as duas primeiras sugestões, no entanto, ainda assim
trabalhosa.
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Precisamos dizer que quais das nossas entidades serão auditadas. Para isso, basta adicionar a
anotação @Audited nas suas entidades. Portanto, para auditar a entidade Conta basta fazer:
@Entity
@Audited
public class Conta {
// atributos e métodos
}
Com isso, o Hibernate Envers vai gerar para cada tabela da aplicação uma tabela com o sufixo _AUD
que guardará as alterações que fizermos nos registros. Para quem está acostumado com um sistema de
controle de versões como CVS ou Subversion, o funcionamento é parecido e para cada alteração que é
feita em uma informação, uma nova versão do registro é gerada na tabela de auditoria.
Não é necessário incluir nenhuma classe nova no classpath, já que o Envers é parte do core do
Hibernate.
Para essa finalidade o Envers disponibiliza um recurso chamado Revision Entity. Trata-se de uma
entidade anotada com @RevisionEntity . Toda vez que uma nova revisão for criada (ou seja, quando
uma entidade auditada for criada ou alterada) uma nova instância da Revision Entity será persistida.
Podemos criar uma classe com os atributos previamente descritos ou podemos optar por estender
org.hibernate.envers.DefaultRevisionEntity que já possui os atributos obrigatórios.
A tarefa de popular os dados da Revision Entity é feita por um listener que implemente a interface
RevisionListener do Envers. Devemos criar esse listener e informá-lo na anotação de nossa Revision
Entity: @RevisionEntity(MeuRevisionListener.class) .
Nesse método recebemos como argumento a instância criada da Revision Entity. Primeiramente
precisamos fazer um casting para a classe que criamos para ser nossa Revision Entity e então podemos
popular nela os dados relativos à revisão que está ocorrendo.
@Entity
@RevisionEntity(MeuRevisionListener.class)
public class MinhaRevisionEntity extends DefaultRevisionEntity{
minhaRevisionEntity.setUserName(nomeUsuario);
}
}
CAPÍTULO 20
"O tempo rende muito quando é bem aproveitado." -- Johann Wolfgang von Goethe
Imagine então que vai chegar na sua aplicação a seguinte String que deve ser buscada: "restaurante
lanche". Nesse caso o usuário está querendo saber quais são as movimentações associadas possium
restaurante ou lanche. Como poderíamos realizar essa busca? Uma primeira ideia é realizar uma query
usando LIKE. Com a JPQL ficaríamos com o seguinte código:
Só que quando formos executar isso, a não ser que tenha uma descrição cujo nome contenha o texto
"restaurante lanche", não será retornado nada no resultado. O primeiro trabalho que teríamos é tratar o
texto de entrada para que possamos separar o texto em várias palavras. Depois, podemos trocar o LIKE
para usar um IN e, com isso, conseguimos testar a descrição que contenha um texto ou outro:
Até resolvemos parcialmente nosso problema. Apesar que, se agora for buscada uma palavra
levemente diferente da que consta no banco, já não conseguimos trazer os resultados. Depois de algum
esforço, até podemos resolver isso, com um subselect por exemplo.
Agora vamos imaginar a seguinte situação: ao procurarmos pela descrição restaurante e lanche
teremos alguns resultados contendo ambas as palavras, alguns contendo apenas a uma palavra
restaurante , outros apenas lanche . Qual seria a ordem ideal para os resultados? Provavelmente os
que contem ambas as palavras são mais relevantes, mas e quanto aqueles que possuem apenas
restaurante OU lanche ? Talvez poderíamos priorizar uma mais "popular", que aparece mais vezes
no banco.
Como fazemos para descobrir quantas vezes o restaurante foi associada a uma movimentacao?
Podemos usar um count como na busca abaixo:
String jpql = "select count(m) from Movimentacao m
where m.descricao = 'restaurante'";
Será performático ter que fazer o count para descobrir a ocorrência de cada uma das categorias que o
usuário buscar? E pior: O que colocaríamos no order by da jpql que faz a busca das movimentações?
Como se a situação não fosse complicada o suficiente, o advento de sites de busca simples e eficientes
como o Google levou os usuários a se habituarem a não precisar acertar a grafia exata de uma palavra
para obter os resultados desejados na busca. Como fazer para possibilitar que o usuário digite
restaurantes , restarante , restaruante ou até rstrante e ainda sim tenha as movimentações
associadas com restaurante ?
Devemos lembrar que existe também a possibilidade do usuário errar a grafia no momento de
cadastrar as movimentações, então pode ser que nosso banco de dados tenha dados incorretos. Nesse
caso mesmo se o usuário digitar corretamente a palavara na busca, as movimentações associadas às
palavrass cadastradas com nomes incorretos não apareceriam no resultado. Esse problema se agrava
pensando que além de possíveis erros d e grafia, deve estar repleta de artigos, preposições etc que não são
relevantes para nossa busca.
Como poderíamos contornar esses problemas, que aparecem no dia a dia de uma pesquisa textual?
Pelo foco que estes projetos dão a parte de busca textual, normalmente eles são chamados de Search
Engines. O mais famoso, e utilizado no mercado, de longe, é o Lucene. Inclusive, vários projetos que
encontramos, que fazem busca textual, são na verdade construídos em cima do Lucene.
Basicamente, o que o Lucene faz é criar uma estrutura de documentos, onde cada documento pode
ter um ou mais campos, e cada campo tem um valor associado. O diferencial dele é conseguir realizar
pesquisas sobre esses documentos seguindo os critérios que queríamos na nossa busca envolvendo a
descrição, isto é, busca textual. Para isso ele vai indexar todo documento de acordo com cada palavra
contida em cada campo seu. Muitas vezes, também podemos ir além com o Lucene e fazer pesquisas
obedecendo a critérios complexos, como: "quero todas as movimentações que contenham restaurante ou
lanche mas não contenham mercado, e que mercado possa estar escrito de várias maneiras".
Alguns consideram o Lucene também como sendo um banco de dados não relacional (NoSQL).
Mas o Hibernate Search não faz parte da especificação que define a JPA 2, então teremos que fazer
algumas configurações específicas do Hibernate para adaptar nosso projeto.
O primeiro trabalho é configurar o Hibernate Search para que ele identifique determinado objeto
como documento do Lucene. Para isso, usamos a anotação @Indexed nas nossas entidades:
@Entity
@Indexed
public class Movimentacao {
}
Agora, precisamos informar quais atributos devem ser adicionados ao documento para que o Lucene
consiga realizar buscas pelos documentos. Para isso, utilizamos a anotação @Field . Supondo que
queremos realizar pesquisas baseadas nas descrições das movimentações, teríamos o seguinte código:
@Entity
@Indexed
public class Movimentacao {
@Field
private String descricao;
}
Aqui temos alguns detalhes. Podemos configurar como o Lucene deve tratar o valor, no caso,
associado à descrição, que vai ser gravado no documento. A ideia é que ele tente gravar isso da melhor
maneira possível para que a busca seja a mais efetiva. Para realizar essa análise, o Lucene faz uso de
algumas classes, que, se quisermos, podemos definir, que são chamada de Analyzers. Por exemplo, para o
Brasil podemos usar o BrazilianAnalyzer , que automaticamente já tiraria expressões como
preposições e artigos das nossas buscas. Por padrão, a implementação utilizada é a StandardAnalyzer .
Essa tarefa de separar cada palavra de determinada texto e analisar é comumente chamada de
tokenização. Cada Analyzer tem os seus tokenizadores.
Após separar cada token, podem ser aplicadas algumas regras para deixá-los ainda mais fáceis de
serem buscados. Em uma situação hipotética, poderíamos ter uma busca que gerasse o token "Roberta",
para melhorar a indexação, o BrazilianAnalyzer ainda retiraria a última vogal, no caso, a letra a. O
objetivo disso é permitir que alguém busque por Roberto e a busca poderia retornar tanto "Roberta" ou
"Roberto". Esse técnica que é aplicada sobre cada token é chamada de stemming. Para informarmos que
nosso atributo deve ser analisado, usamos o atributo index da anotação @Field :
@Entity
@Indexed
public class Movimentacao {
Uma questão importante, é que todo atributo anotado com @Id automaticamente já é atribuído ao
documento indexado pelo Lucene. Os documentos do Lucene também possui uma id, como os registros
no banco de dados. Hibernate Search usará a mesma id do registro para o documento. O Hibernate faz
isso para que, após realizar a pesquisa textual, consiga recuperar o objeto associado a determinado id no
banco de dados.
Hibernate Search também define uma anotação @DocumentId que pode ser omitida já que o
atributo com @Id automaticamente define a id do documento.
Por padrão, o Lucene não guarda o texto original após tokenizá-lo. Ele faz isso basicamente para
otimizar o espaço e a quantidade de informação que deve ser recuperada após a busca. Se quisermos
mudar esse comportamento, podemos usar o atributo store da anotação @Field :
@Entity
@Indexed
public class Movimentacao {
@Field(store=Store.YES)
private String descricao;
}
Uma questão que poderia surgir é que queremos indexar os nomes das categorias associadas a cada
movimentação. Precisamos informar que os valores dos atributos dos objetos do tipo Categoria
devem ser indexados pelo Lucene. Para isso, existe uma anotação chamada de @IndexedEmbedded .
Como cada movimentação contém uma lista de categorias associadas, vamos anotar este atributo com
@IndexedEmbedded . Temos agora o seguinte código:
package br.com.caelum.financas.modelo;
@Entity
@Indexed
public class Movimentacao {
@Field
private String descricao;
@ManyToMany
@IndexedEmbedded
private List<Categoria> categorias = new ArrayList<Categoria>();
}
Agora, na classe Categoria , precisamos informar quais atributos devem ser indexados. Seguimos a
mesma ideia de quando anotamos a descrição da movimentação com @Field :
package br.com.caelum.financas.modelo;
@Entity
public class Categoria {
@Id
@GeneratedValue
private Integer id;
@Field
private String nome;
Com isso, agora é sabido que quando alguma movimentação for sofrer alguma operação que reflita
no estado do banco de dados, o Hibernate vai sincronizar o estado dele com os documentos do Lucene.
Para cada movimentação será criado um documento do Lucene. Dentro desse documento teremos a id,
a descrição e uma lista de categorias (id e nome da categoria).
Todas essas configurações devem ser realizadas no persistence.xml. Aqui temos as que são
necessárias para a nossa aplicação:
<property name="hibernate.search.default.directory_provider"
value="filesystem" />
<property name="hibernate.search.default.indexBase"
value="lucene/indexes" />
<property name="hibernate.search.analyzer"
value="org.apache.lucene.analysis.standard.StandardAnalyzer"/>
Todas essas configurações são específicas para o Hibernate Search e vão ser ignoradas por qualquer
outra implementação da JPA 2 que venha a ser utilizada. Vamos detalhar elas:
hibernate.search.default.indexBase indica qual pasta deve ser usada como base para
armazenar os índices.
<property name="hibernate.search.default.directory_provider"
value="filesystem" />
<property name="hibernate.search.default.indexBase"
value="/home/jpaXXXX/workspace/fj25-financas-web/WebContent/WEB-INF/
lucene/indexes" />
<property name="hibernate.search.analyzer"
value="org.apache.lucene.analysis.standard.StandardAnalyzer"/>
2. Agora, vamos adicionar as anotações necessárias para configurarmos nossas classes a serem
indexadas através do Hibernate Search. No nosso caso, queremos indexar a descrição da
movimentação. Vamos adicionar as anotações @Indexed na classe Movimentacao e @Field no
atributo descricao . Os imports devem ser realizados do pacote
org.hibernate.search.annotations .
package br.com.caelum.financas.modelo;
//import omitidos
//outras anotacoes
@Entity
@Indexed
public class Movimentacao {
@Field
private String descricao;
3. No banco de dados já temos algumas categorias associadas às movimentações. Precisamos fazer com
que o Hibernate Search consiga indexar todas essas categorias.
@Inject
EntityManager manager;
Aperte o botão para inicializar a indexação. Depois atualize o projeto no Eclipse e verifique a pasta
WebContent/WEB-INF . Nela deve se encontrar uma pasta lucene que possui os documentos
criados na indexação.
EntityManager em = ...
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
Antes de criarmos a nossa busca de verdade, precisamos de um objeto que consiga pegar os termos
pesquisados e aplicar as mesmas regras que foram utilizadas durante o processo de indexação. Caso as
regras aplicadas para o processo de busca sejam diferentes, fatalmente o resultado não vai ser o esperado,
pois será realizada uma busca por termos que não foram indexados. Para isso, utilizamos um objeto do
tipo QueryParser :
FullTextEntityManager fullTextEntityManager =
Search.getFullTextEntityManager(manager);
Analyzer analyzer = fullTextEntityManager.getSearchFactory()
.getAnalyzer( Movimentacao.class );
QueryParser parser = new QueryParser("descricao",analyzer);
Passamos alguns parâmetros para o QueryParser . O primeiro é nome do campo que queremos
pesquisar no documento. A regra padrão utilizada para criação dos campos no documento é a do nome
do atributo anotado com @Field . No caso da Movimentacao , temos o atributo descricao anotado
com @Field , ou seja temos descricao O segundo e último parâmetro indica o Analyzer que vamos
utilizar para parsear o termo pesquisado. Para obter o Analyzer default para a classe Movimentação
usamos o método getAnalyzer() .
Com um objeto do tipo QueryParser , podemos criar uma consulta que deve ser realizada nos
documentos para trazer todos que atenderem aos critérios especificados. Através do método parse da
classe QueryParser , criamos um objeto do tipo Query que representa a nossa consulta. Vamos
apenas tomar cuidado para não confundir essa classe com a javax.persistence.Query ; o pacote
referente a essa classe Query que vamos usar é o org.apache.lucene.search :
QueryParser parser = new QueryParser("descricao",analyzer);
Query query = parser.parse("almoço");
É interessante notar que usamos o mesmo método getResultList usado anteriormente. Isso
porque FullTextQuery apenas implementa a interface javax.persistence.Query provida pela JPA
2.
Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam
tecnicamente o assunto para revisar os livros a fundo. Não têm anos de
experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e
obsessão por livros de qualidade a preços justos.
Mas, por exemplo, quando indexamos a descrição e as categorias da movimentação, se para nosso
usuário for mais relevante encontrar os termos da busca na descrição do que nas categorias, o que
podemos fazer? Sabemos que podemos influenciar a relevância usando o boost em nossas buscas. Se
quisermos que em nossas buscas a relevância de encontrar os termos na descrição da movimentação seja
duas vezes maior que encontrar nas categorias podemos configurar um boost para ser aplicado no
campo descrição nos índices.
@Field(indexNullAs="nenhuma", boost=@Boost(2f))
private String descricao;
A decisão de delegar para o usuário a responsabilidade de entender a sintaxe do Lucene nem sempre
é possível, então como fazer para possibilitar uma busca como
"alimntação~0.3 familia^2 NOT trabalho"
Para resolver esse problema podemos criar componentes visuais na página para que o usuário
escolha opções avançadas em menus, como:
Tendo criado os componentes, precisaríamos gerar a query para o Lucene a partir das escolhas que o
usuário fez. Escrever um código que gera uma String concatenando os termos digitados pelo usuário os
modificadores do Lucene é perfeitamente possível. Felizmente o Hibernate Search n os provê uma
ferramenta para facilitar a execução essa tarefa.
No que se refere a buscas textuais, uma alternativa ao uso do Lucene query parser é o uso do
Hibernate Search DSL . Nessa forma de busca a query é gerada a partir do QueryBuilder de forma
programática, mais próxima à API de Criteria da JPA2.
QueryBuilder queryBuilder =
fullTextEntityManager.getSearchFactory().buildQueryBuilder()
.forEntity( Movimentacao.class ).get();
Para criar a query precisamos passar para o builder a String que desejamos buscar e em qual parte
do documento ( field ) desejamos fazer a busca. Se desejarmos fazer a busca pela descrição teríamos o
código:
Para fazer a busca em mais de um field basta criar a query usando o método onFields , que
aceita um ou mais campos String (um var-arg de String).
254 20.12 PARA SABER MAIS: UTILIZANDO O HIBERNATE SEARCH DSL PARA FACILITAR BUSCAS AVANÇADAS
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
Precisamos agora fazer o wrapping da Query gerada pelo Hibernate Search DSL em uma Query da
JPA, com isso ganhamos os métodos que conhecemos, como o getResultList .
Se quisermos migrar o método de nosso DAO para utilizar o Hibernate Search DSL teremos o
código:
public List<Movimentacao> buscaMovimentacoesBaseadoNaDescricao(String texto) {
FullTextEntityManager fullTextEntityManager =
Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder =
fullTextEntityManager.getSearchFactory().
buildQueryBuilder().forEntity( Movimentacao.class ).get();
org.apache.lucene.search.Query query =
queryBuilder.keyword()
.onField("descricao")
.matching(texto).createQuery();
return jpaQuery.getResultList();
}
O problema de fazer essa mudança é que a capacidade que o usuário tinha de usar a sintaxe do
Lucene em suas buscas é perdida (no processo de tokenização os termos NOT, ^3 e outros serão
retirados).
Agora faremos a inclusão das opções de busca avançadas programaticamente. Se quisermos, por
exemplo, aumentar a relevância dos termos de uma query podemos usar o método boostedTo . O
exemplo de código abaixo ilustra o uso do método:
TermContext termContext = queryBuilder.keyword();
termContext = termContext.boostedTo(2f);
Query query = termContext.onField("descricao").matching(texto).createQuery();
A Hibernate Search DSL , assim como a API de Criteria da JPA2, foi concebida usando o
conceito de interface fluente. Podemos unir o boosting com Fuzzy da seguinte maneira:
20.12 PARA SABER MAIS: UTILIZANDO O HIBERNATE SEARCH DSL PARA FACILITAR BUSCAS AVANÇADAS 255
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
junction.must(criaQuery(em, termo0));
junction.must(criaQuery(em, termo1)).not();
//...
}
return criaBuilder(em).keyword()
.fuzzy()
.withThreshold(elemento.getSemelhanca().getValor())
.boostedTo(elemento.getMultiplicador())
.onField("descricao")
.matching(elemento.getTexto()).createQuery();
}
256 20.13 PARA SABER MAIS: BUSCANDO TERMOS USANDO AND E NOT
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
20.13 PARA SABER MAIS: BUSCANDO TERMOS USANDO AND E NOT 257
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.
CAPÍTULO 21
"O dinheiro não traz felicidade - para quem não sabe o que fazer com ele." -- Machado de Assis
O hibernate na maioria das vezes possui todas as funcionalidade descritas na JPA e mais um pouco,
algumas vezes ele até possui a funcionalidade descrita na JPA implementada em mais de uma maneira.
No fundo as duas tecnologias fazem a mesma coisa, porém quando olhamos para o hibernate, sem as
amarras impostas pela JPA, obtemos uma ferramenta com muito mais funcionalidades.
Algumas dessas funcionalidade a mais faz com que o hibernate se torne um forte opção na hora de
escolher uma ferramenta ORM, muitas vezes até mais usada que a JPA.
Um esqueleto de hibernate.cfg.xml:
<hibernate-configuration>
<session-factory>
<property name="connection.url">jdbc:mysql://localhost/teste</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.username">root</property>
<property name="connection.password"></property>
<property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
<property name="hbm2ddl.auto">update</property>
<property name="show_sql">true</property>
<property name="format_sql">true</property>
As propriedade que passamos para o hibernate.cfg.xml são muito parecidas com as passadas para o
persistence.xml.
Para conseguir uma session precisamos de uma SessionFactory , que por sua vez precisa de um
objeto do tipo Configuration (nas versões anteriores tínhamos que usar o
AnnotationConfiguration .
Com uma Session na mão, podemos inserir, atualizar/gerenciar, deletar e carregar objetos de
maneira muito parecida com o EntityManager :
Uma diferença para o método persist da JPA é que o aqui no save, mesmo que o objeto tenha
um id e o campo seja @GeneratedValue o hibernate vai ignorá-lo ao invés de lançar uma exceção.
Exemplo de uso o método update para uma atualizar e gerenciar um objeto no banco:
Conta c = new Conta();
c.setId(1);
c.setAgencia("123");
c.setBanco("BB");
c.setNumero("1231-1");
c.setTitular("Outro Joao da Silva");
Uma diferença entre o update do hibernate e o merge da JPA é que o update torna o objeto
passado como parâmetro gerenciado e o merge devolve uma cópia gerenciada, mas não gerencia o
objeto que foi passado.
Uma observação importante aqui é que diferente da JPA a entidade passada para o delete não
JPA Hibernate
persist save
merge update
remove delete
getReference load
find get
refresh refresh
As sessões ainda possuem um método a mais muito importante, é o evict . Ele tira do estado
managed um único objeto, tornando-o detached.
Movimentacao m = (Movimentacao) session.get(Movimentacao.class, 1l);
session.evict(m);
Pegando algumas queries escritas no capítulo de JPQL vamos traduzi-las para criteria do hibernate.
Com criteria ao invés de criar um objeto Query , criamos um objeto do tipo Criteria , passando
em qual tabela ele buscará os resultados:
Criteria criteria = session.createCriteria(Movimentacao.class);
Este objeto criado já é suficiente para devolver todas as movimentações, basta chamar o método
list :
Criteria c = session.createCriteria(Conta.class);
return c.list();
}
Adicionando uma cláusula para obter apenas as contas com valor maior que algum valor temos o
seguinte código na JPQL:
public List<Movimentacao> movimentacoesComValorMaiorQue(double valor){
String sql = "from Movimentacao m where m.valor > :valor";
Query q = entityManager.createQuery(sql);
q.setParameter("valor", valor);
return q.getResultList();
}
Para fazer uma consulta semelhante com criteria, devemos adicionar uma Restriction à criteria,
usando o método add e a fábrica Restrictions :
eq => igual
lt => menor
gt => maior
le => menor igual
ge => maior igual
not => negação
like => texto igual
ilike => texto igual, ignorando maiúscula ou minúscula
between => dentro de um intervalo
Para a criteria:
public List<Movimentacao>
movimentacoesComValorMaiorQueEDescricao(double valor, String descricao){
Criteria criteria = session.createCriteria(Movimentacao.class);
criteria.add(Restrictions.gt("valor", valor));
criteria.add(Restrictions.like("descricao", descricao));
return criteria.list();
}
Até podemos adicionar paginação neste código de maneira muito simples e idêntica à JPQL:
public List<Movimentacao>
movimentacoesComValorMaiorQueEDescricao
(double valor, String descricao, int primeiro, int tamanhoDaPagina){
return q.getResultList();
}
Talvez pudéssemos até melhorar algum if ou outro, mas isso também nos geraria bastante trabalho,
porém com criteria este trabalho já foi feito:
public List<Movimentacao>
movimentacoesComValorMaiorQueEDescricao(double valor, String descricao){
Criteria criteria = session.createCriteria(Movimentacao.class);
if(valor > 0){
criteria.add(Restrictions.gt("valor", valor));
}
if(descricao != null && !descricao.isEmpty()){
criteria.add(Restrictions.like("descricao", descricao));
}
return criteria.list();
}
O hibernate ainda possui uma outra opção interessante para fazer buscas dinâmicas é a Query By
Example. Com query by example podemos passar um modelo do objeto que estamos procurando para
criteria que automaticamente o hibernate ignora os atributos não preenchidos e faz uma pesquisa por
aquilo que estiver preenchido.
Já com criteria temos um passo a mais, juntar as duas restrições através da cláusula or , o or
também é obtido através da fábrica Restrictions :
public List<Movimentacao>
movimentacoesComValorMaiorQueEDescricao(double valor, String descricao){
Criteria criteria = session.createCriteria(Movimentacao.class);
Criterion criterion1 = Restrictions.gt("valor", valor);
Criterion criterion2 = Restrictions.like("descricao", descricao);
Criterion or = Restrictions.or(criterion1, criterion2);
criteria.add(or);
return criteria.list();
}
Até poderíamos reduzir o código utilizando chamadas inline e o import static do java 5:
public List<Movimentacao>
movimentacoesComValorMaiorQueEDescricao(double valor, String descricao){
Criteria criteria = session.createCriteria(Movimentacao.class);
criteria.add(or(
gt("valor", valor),
like("descricao", descricao)
));
return criteria.list();
}
Se quiséssemos adicionar várias restrição ligadas por or este código se complicaria muito, por isso
temos a disjunction , que pode ser interpretada como uma lista de restrições ligadas por or :
Projections
Com JPQL para buscarmos a soma das movimentações usaríamos o seguinte código:
public BigDecimal somaDaMovimentacoes(){
String sql = "select sum(m.valor) from Movimentacao m"
Query q = entityManager.createQuery(sql);
return (BigDecimal) q.getSingleResult();
}
Com criteria temos que usar a classe Projections para isso, ela é uma fábrica de projeções , com
isso podemos escolher o que será retornado da pesquisa:
public BigDecimal somaDaMovimentacoes(){
Criteria criteria = session.createCriteria(Movimentacao.class);
criteria.setProjection(Projections.sum("valor"));
return (BigDecimal) criteria.uniqueResult();
}
Se precisássemos mais de um valor de retorno, por exemplo a soma e a contagem deveríamos usar
uma lista de projeções:
public Object[] somaEQuantidadeDeMovimentacoes(){
Criteria criteria = session.createCriteria(Movimentacao.class);
ProjectionList list = Projections.projectionList();
list.add(Projections.sum("valor"));
list.add(Projections.rowCount());
criteria.setProjection(list);
return (Object[]) criteria.uniqueResult();
}
Em ambos os casos o retorno é um array de Object, na primeira posição a soma dos valores e na
segunda posição a quantidade de movimentações.
Os ResultTransformer 's podem ser usados tanto com HQL como Criteria, desde que usemos a
Session do hibernate. Esta funcionalidade permite ao programador executar um código em cada
elemento da lista, ou mesmo executar um código na lista inteira antes da lista ser devolvida pelo método
list .
Pegando a consulta da seção anterior, que devolve um Object[] poderíamos devolver algo mais
prático, como por exemplo a classe abaixo:
class SomaETotal {
private BigDecimal soma;
private Long total;
}
}
Essa interface nos obriga a ter um método chamado transformTuple , que serve para manipular
cada resultado separadamente e também o transformList , que trabalha com a lista inteira de uma vez.
Cada resultado é transformado e devolvido através de um objeto SomaETotal . Poderíamos até ter
usado o segundo argumento do método transformTuple para ler o nome que o banco devolveu de
cada coluna de resultado, para fazer algo mais dinâmico.
Agora basta passar esse ResultTransformer para a Criteria (lembrando que também
funcionaria com uma Query desde que fosse através da Session .
Perceba que o código abaixo demos um alias para cada projection, assim o hibernate sabe qual
setter chamar.
Com JPQL foi muito simples de realizar uma pesquisa com join.
Já com criteria temos um trabalho a mais, precisamos criar uma criteria filha da criteria principal
(Não confundir com subselect), essa criteria filha é responsável pelas buscar na tabela que será feito o
join.
public List<Movimentacao> movimentacoesDoTitularDaConta(String titular){
Criteria principal = session.createCriteria(Movimentacao.class);
Criteria conta = principal.createCriteria("conta");
conta.add(Restrictions.like("titular", titular));
return principal.list();
}
Com um mesmo efeito poderíamos dar um apelido para a criteria de conta e usado a criteria
principal para adicionarmos a restrição:
Criteria principal = session.createCriteria(Movimentacao.class);
principal.createAlias("conta", "c");
principal.add(Restrictions.like("c.titular", titular));
return principal.list();
Ainda no falando de join, com JPQL explicitamos a fetch do join através do join fetch :
public List<Conta> todasAsContasComMovimentacaoPreenchidas(){
String jpql = "from Conta c "+
" inner join fetch c.movimentacoes "+
" where c.titular like :titular";
Query query = entityManager.createQuery(jpql);
query.setParameter("titular", titular);
return query.getResultList();
}
Com criteria também podemos fazer isso, temos o setFetchMode para configurar o fetch do
relacionamento.
public List<Conta> todasAsContasComMovimentacaoPreenchidas(){
Criteria principal = session.createCriteria(Conta.class);
principal.add(Restrictions.like("titular", titular));
principal.setFetchMode("movimentacoes", FetchMode.JOIN);
return principal.list();
}
grandes para executar. Com hibernate cada resultado da busca acaba se tornando um objeto na
memória, com buscas grandes o consumo de memória é bem grande.
Com JDBC podemos ter um caso diferente, pois o ResultSet não devolve todos os resultado de
uma vez, o que ele devolve é um cursor para os resultados que estão no banco ainda. Cada vez que
chamamos um next no ResultSet o movemos o cursor para o próximo resultado, com isso não
precisamos guardar todos os resultados em memória, podemos processar cada um separadamente,
consumindo a memória de um resultado pode vez.
Para usar o ScrollableResults basta que ao invés de chamar o método list chamemos o
método scroll , ele retornará um objeto do tipo ScrollableResults .
Criteria criteria = session.createCriteria(Movimentacao.class);
ScrollableResults results = criteria.scroll();
O uso do ScrollableResults é muito parecido com o ResultSet , temos o método next que
move o cursor para o próximo resultado devolvendo um boolean caso tenha conseguido ir para o
próximo.
Cada linha de resultado possui métodos para recuperar as informações vindas do banco. Porém cada
linha de resultado não vêm em forma de tupla do banco (separado por colunas) igual ao ResultSet os
resultados obtidos pelo ScollableResults são entidades já em formato de objeto. Cada resultado é na
verdade um Object[] onde cada posição é preenchido com o que foi pedido na consulta, se pedimos
só Movimentacao esse array estará com apenas a primeira preenchida com uma Movimentacao .
Podemos acessar esse array através do método get ou podemos usar outro método get , mas
passando um índice que ele já retornará o conteúdo do array daquele índice.
Criteria criteria = session.createCriteria(Movimentacao.class);
ScrollableResults results = criteria.scroll();
while(results.next()){
Movimentacao m = (Movimentacao) results.get(0);
}
Caso tenhamos pedido o count e a soma, cada array de resultado terá dois objetos dentro, um no
índice 0 e outro no índice 1. No ScrollableResults se os tipos que pedimos na consulta forem
comuns, temos alguns métodos que evitam o casting.
projectionList.add(Projections.rowCount());
projectionList.add(Projections.sum("valor"));
criteria.setProjection(projectionList);
ScrollableResults results = criteria.scroll();
while(results.next()){
Long count = results.getLong(0);
BigDecimal soma = results.getBigDecimal(1);
Com criteria também podemos realizar subselect's, para isso temos que usar as
DetachedCriteria 's. DetachedCriteria pode ser considerada uma criteria que ainda não está presa
à uma session. Com isso podemos escrever o _subselect` com ela como se fosse uma criteria normal e
depois adicionarmos a criteria real.
21.9 STATELESSSESSION
Alguns sistema, trabalham com recebimento de arquivos online, esses arquivo podem ser xml,
posicional, CSV(comma separated values) e seus tamanhos podem chegar a ter vários _Gigas::, 2Gb,
3Gb.
<movimentacoes>
<movimentacao>
<descricao>Movimentacao 1</descricao>
<data>2000-09-12</data>
<valor>1000.12</valor>
</movimentacao>
</movimentacoes>
Até poderíamos ter uma classe para ler movimentação por movimentação de um arquivo maior:
while(leitor.temProximaMovimentacao()){
session.save(leitor.proximaMovimentacao();
}
Porém se o arquivo que estamos lendo tiver muitas movimentações, podemos rapidamente estourar
a memória da máquina virtual. Mas por quê?
Porque cada movimentação que salvamos a session gerencia este objeto, nesse caso se na mesma
sessão salvarmos 1 milhão de objetos, a sessão gerenciará esses 1 milhão de objetos e os conservará na
memória, provavelmente desnecessariamente pois essa é uma operação em lote e não vamos mexer nos
objetos neste instante.
Nesses casos o ideal seria que pudéssemos salvar os objetos porém que eles não ficassem gerenciados.
É exatamente isso que o StatelessSession faz. Ele possui todas acesso a todas os mapeamentos que
foram feitos, porém ele não gerencia o ciclo de vida das entidades.