Neste são apresentadas melhores práticas para uso do framework Hibernate dentro de aplicações web, e para lidar com transações da aplicação utilizando técnicas de “lock otimista”. As práticas são ilustradas com um exemplo completo, útil em várias empresas: uma aplicação de reserva de salas de reuniões. (Com poucas modificações, principalmente na entrada de dados, a mesma aplicação poderia lidar com reservas de assentos em teatros, lugares em vôos, quartos de hotéis e outros cenários similares.)

A aplicação será desenvolvida respeitando boas práticas do desenvolvimento JEE, em especial a separação em camadas de apresentação, negócio e persistência, e a arquitetura MVC. O quadro “Camadas em uma aplicação web” relembra e relaciona esses conceitos. Não será utilizado outro framework além do Hibernate, embora as técnicas apresentadas aqui sejam aplicáveis a aplicações baseadas no Struts, JSF, Spring, WebWork e outras tecnologias. E apesar do foco em aplicações web, as práticas e o código referente ao Hibernate (para a camada de persistência) poderiam ser utilizados sem modificações em aplicações não-web.

Vamos utilizar o Tomcat 5.5, e o JSTL 1.1 (através da implementação Jakarta Taglibs Standard; veja as referências ao final do artigo). Para o leitor que não acompanhou o artigo sobre o Hibernate na Edição 28, o quadro “Conceitos essenciais do Hibernate” fornece um resumo das principais classes e arquivos de configuração do framework.

Sessions do Hibernate x containers web

Antes de apresentar detalhes do exemplo, precisamos discutir alguns conceitos importantes para o entendimento da nossa aplicação.

A adequação do Hibernate a uma aplicação web envolve quatro atividades fundamentais:

  1. Criação de um SessionFactory.
  2. Obtenção e liberação de conexões ao banco de dados.
  3. Criação e fechamento de um Session[1].
  4. Demarcação de início e fim de um Transaction no banco de dados.

A criação de um SessionFactory é um processo caro, assim o ideal é realizá-lo uma única vez para toda a aplicação. O mesmo SessionFactory pode ser compartilhado por múltiplos threads: só necessitamos de um local onde colocar este objeto, que seja visível aos demais objetos dentro da aplicação.

Então basta identificar um local (e um momento) apropriado para criar o SessionFactory dentro da aplicação web, e estabelecer uma forma de compartilhar esta única instância entre todas as classes de persistência da aplicação. O SessionFactory também é o responsável por gerenciar conexões ao banco de dados feitas pelo Hibernate, assim a definição de como realizar a primeira atividade implica em definir também a segunda.

O Session e o Transaction geralmente andam juntos, pois uma transação deve ser iniciada e completada com uma mesma sessão aberta. Uma única sessão pode ser utilizada para várias transações, mas isto traz o risco de que a aplicação utilize versões obsoletas dos objetos persistentes que são gerenciados pela sessão. Além disso, uma sessão pode conter uma grande quantidade de objetos e mantê-los abertos e vinculados à sessão HTTP, para atender a várias requisições de um mesmo usuário. Isso pode prejudicar seriamente performance da aplicação em ambientes de cluster. Existem cenários válidos para o que a documentação do Hibernate chama de “sessões longas” (long sessions), onde uma única sessão engloba várias transações, mas o autor (e os criadores do Hibernate) prefere não usar esta prática.

Dessa forma, iremos impor que todas as sessões sejam criadas e fechadas dentro da mesma requisição HTTP, e a definição das atividades (3) e (4) se resume também em identificar o local e o momento apropriado para iniciar e fechar simultaneamente um Session e um Transaction.

Pode ser muito demorado reconstruir objetos persistentes complexos, compostos por vários objetos agregados, a cada requisição HTTP – digamos entre a construção da página JSP para a edição de um conjunto de objetos e a execução do servlet que efetivamente salva as alterações no banco de dados. Nesses casos, pode ser configurado no Hibernate um “cache de segundo nível” (second-level cache), que não é vinculado à sessão HTTP ou ao Session do Hibernate, mas sim à JVM que roda o container web como um todo. Este cache não é replicado entre os nós de um cluster, e por isso tem um impacto pequeno sobre a performance. Apenas deve-se tomar o cuidado de configurar um provedor de cache capaz de operar em ambiente de cluster, como o OSCache ou o JBoss Cache.

Agora que já sabemos as restrições desejadas sobre a implementação das quatro atividades fundamentais, vamos descrever as duas formas mais comuns de realizar essas atividades. A primeira usa os recursos da API de Servlets. A segunda utiliza o pattern Singleton[2] e a classe ThreadLocal do Java SE. Elas são comparadas na Tabela 1 e detalhadas a seguir.

Atividade Usando a API de Servlets Usando uma classe utilitária
Criação de um SessionFactory ServletContextListener armazena o SessionFactory no ServletContext (contexto web) Métodos estáticos armazenam o SessionFactory como um Singleton
Obtenção de uma conexão ao banco de dados hibernate.cfg.xml referencia um DataSource JNDI hibernate.cfg.xml referencia um DataSource JNDI
Abertura e Fechamento de um Session Servlet controlador ou Filter armazena o Session no HttpServletRequest Métodos estáticos armazenam o Session em um ThreadLocal
Abertura e Fechamento de um Transaction Servlet controlador ou Filter armazena o Transaction no HttpServletRequest Métodos estáticos armazenam o Transaction em um ThreadLocal

Tabela 1. Duas maneiras comuns de usar o Hibernate dentro de aplicações web

A segunda alternativa é a preferida pelos criadores do Hibernate, e é a que usaremos na aplicação de exemplo. Ela tem o benefício de ser portável para aplicações não-web, embora exija um pouco mais de cuidado e disciplina do programador. Aplicações não-web podem não ter acesso a um serviço JNDI para obter DataSources, mas esta questão pode ser resolvida editando-se o arquivo de configuração principal do Hibernate.

Por outro lado, existem cenários válidos para a primeira alternativa, que é em geral a escolhida pelos desenvolvedores iniciantes. Ela traz o benefício de permitir o uso da “persistência transparente” do Hibernate por páginas JSP e outros componentes de visualização, em vez de restringi-los às classes de negócio e de persistência, como é imposto pela primeira alternativa.

Usando a API de Servlets para gerenciar o Hibernate

A primeira alternativa usa recursos da API de Servlets. Versões mais recentes dessa API (desde a 2.3) permitem configurar um ServletContextListener , que é chamado sempre que o contexto web for criado e destruído, correspondendo à ativação e ao término da aplicação web. Com isso, na ativação da aplicação pode-se criar o SessionFactory e depois destruí-lo quando a aplicação terminar. Deixamos o SessionFactory acessível ao restante da aplicação colocando-o em um atributo do contexto web.

Para criar um SessionFactory é necessário fornecer um objeto Configuration que lê do arquivo de configuração do Hibernate (hibernate.cfg.xml) os parâmetros para a conexão ao banco de dados. Boas práticas para o desenvolvimento web recomendam o uso de um pool de conexões, para melhorar a escalabilidade da aplicação. O Hibernate é capaz de gerenciar um pool internamente ou de utilizar o pool gerenciado pelo container web. O quadro “Pools da aplicação e do container” apresenta vantagens e desvantagens de cada abordagem. Aqui serão adotados os pools gerenciados pelo container. Os detalhes desta configuração são apresentados no final do artigo.

Em aplicações que utilizam um único servlet como controlador, este servlet pode criar e fechar a sessão e passá-la como argumento para os objetos de negócio dentro do modelo. Já em aplicações que utilizem vários servlets controladores, pode ser definido um filtro (javax.servlet.http.Filter) para criar e destruir a sessão, que é passada para os servlets como um atributo da requisição. Esta segunda opção também se aplica a situações em que o controlador é fornecido pelo framework e não seja fácil acrescentar um código genérico de início e final de tratamento das requisições.

Depois que o Session e o Transaction são inseridos como atributos da requisição, eles devem ser recuperados pelo controlador e repassados como argumentos para os métodos das classes de negócio e de persistência.

A Listagem 1 ilustra o código e a configuração para esta abordagem, apresentando o listeners e o filtro utilizados para gerenciar os objetos SessionFactory, Session e Transaction do Hibernate.

Usando Singleton e ThreadLocal para gerenciar o Hibernate

A segunda alternativa faz o uso do pattern Singleton: uma classe utilitária mantém uma única instância de SessionFactory, e as classes de persistência chamam esta classe utilitária para obter o SessionFactory e então criar o Session, conforme a necessidade.

Mas como garantir um único Session e um único Transaction para várias classes de persistência que são executadas dentro da mesma requisição HTTP? Não podemos utilizar outro singleton para estes objetos, pois são necessárias várias instâncias de cada um, para usuários simultâneos da aplicação.

A recomendação dos criadores do Hibernate é usar a classe java.lang.ThreadLocal da biblioteca padrão do Java SE. Essa classe cria uma área de armazenamento de objetos particular para um thread. Assim, se for colocado um Session do Hibernate dentro de um ThreadLocal, o mesmo Session poderá ser recuperado de lá por qualquer outro método executado no mesmo thread, evitando que as classes de negócio e de persistência tenham que fornecer alguma maneira de passar o objeto Session adiante, por meio de argumentos de métodos ou atributos estáticos. (Pode-se considerar que um ThreadLocal é na prática equivalente a um atributo de requisição da API de Servlets – mas o seu uso não exige um container web.)

Além disso, operações realizadas em uma mesma transação do banco de dados têm que ser obrigatoriamente executadas pelo mesmo thread; então classes de persistência podem presumir que, se já existe um Session no ThreadLocal, é porque essas classes estão sendo utilizadas como parte de uma transação iniciada em outra classe de persistência.

Nossa classe utilitária pode encapsular o uso de ThreadLocal, fornecendo às classes de negócio ou de persistência métodos estáticos para abrir, fechar e recuperar um Session (e um Transaction).

A Listagem 2 apresenta o código que implementa esta segunda alternativa, e que é parte da aplicação de exemplo apresentada mais adiante. A classe utilitária será útil sem modificações tanto para aplicações web quanto para aplicações não-web, de modo que as mesmas classes de persistência e de negócio poderão ser utilizadas nos dois tipos de aplicações.

A única preocupação do desenvolvedor ao usar essa abordagem é garantir que, para toda operação de negócio, haja um método que realiza a operação – possivelmente delegando a maior parte do trabalho para outros métodos de negócio ou de persistência. Em geral isto é feito utilizando-se o pattern Facade[3] ou Fachada. Cada método da Fachada cuidaria então de iniciar e encerrar as transações e sessões do Hibernate, chamando os métodos da classe utilitária. Dessa forma, temos uma camada de negócio totalmente autônoma, e uma camada de apresentação que não tem nenhuma amarração desnecessária com a camada de persistência.

Com esta abordagem (usando ou não o pattern Fachada) as classes de negócio e de persistência são obrigadas a recuperar explicitamente todos os dados (especialmente atributos e relacionamentos configurados para lazy loading) – utilizando por exemplo a clausula join fetch do HQL. Caso isso não seja feito, ocorrerão LazyInitializationExceptions do Hibernate na camada de apresentação.

Servlets, Singletons e separação entre as camadas

Ao apresentar a primeira alternativa de arquitetura, vimos que, através de um ServletContextListener , pode ser criado o SessionFactory da aplicação – enquanto que um filtro (ou servlet) pode iniciar e fazer o encerramento tanto de um Session quanto de um Transaction do Hibernate, no início e final da requisição HTTP. Entretanto, esta estratégia não é inteiramente satisfatória, pois ela obriga uma classe derivada da API de Servlets (no caso, uma classe de apresentação) a lidar com uma classe do Hibernate (uma classe de persistência). Estaríamos assim violando a separação entre as duas camadas.

O uso do Hibernate deveria ser um detalhe de implementação da camada de persistência da aplicação, então o seu uso não deveria ser percebido por classes de camadas superiores. Mas sendo a sessão do Hibernate criada por um servlet ou filtro, ela deverá ser passada pela camada de apresentação como argumento para os métodos das classes de negócio. E as classes de negócio, por sua vez, devem passar a sessão como argumento para os métodos das classes de persistência. Da mesma forma, as classes de negócio e de persistência não deveriam “enxergar” nem o HttpServletRequest nem o ServletContext para obter os atributos armazenados neles. Mas se for utilizada a API de Servlets, não há como fugir da passagem explícita destes objetos desde a camada de apresentação até a camada de persistência.

Na segunda alternativa de arquitetura, o uso do singleton e do ThreadLocal evita que classes de apresentação tenham de se preocupar com o uso do Hibernate, e também evita que a interface das classes de negócio ou de persistência sejam poluídas com argumentos adicionais, para que a camada de apresentação possa passar a sessão e transação do Hibernate.

Infelizmente essa segunda alternativa também tem suas desvantagens: ela não fornece um meio fácil para se delimitar os momentos de abertura e fechamento do Session e do Transaction. Se um método de uma classe de persistência realizar a abertura e o fechamento, ele não poderá ser utilizado como parte do processamento de um método de negócio que envolva outros métodos de persistência. Por outro lado, se o método de persistência não fizer a abertura e o fechamento, ele não poderá ser utilizado sem que o chamador precise “se lembrar” de realizar estas atividades, aumentando a chance de se cometer erros de programação na aplicação.

Uma forma de lidar com esse problema é obrigar todos os métodos de classes de persistência a serem chamados pelas classes de apresentação de forma indireta. Este é o pattern Fachada citado anteriormente. Assim sempre haveria "no meio do caminho” um método de uma classe de negócio, que seria responsável por abrir e fechar o Session e o Transaction. Esta é a solução adotada na aplicação de exemplo deste artigo. Mas consulte também o quadro “Quando violar a separação entre camadas” para uma discussão sobre uma terceira solução, que “mistura” as duas alternativas apresentadas e pode ser a preferida por alguns desenvolvedores.

Com isso concluímos nossas discussões iniciais sobre possíveis arquiteturas. Veja na Tabela 2 uma nova comparação, apresentando vantagens e desvantagens de cada alternativa.

Vantagens Desvantagens
Usando a API de Servlets
  • Classes de negócio e de persistência não necessitam delimitar o início e o final de sessões e transações.
  • O uso de filtros é independente do framework MVC adotado.
  • Classes de apresentação ficam poluídas com código que seria de persistência.
  • As interfaces das classes de negócio e de persistência ficam poluídas com argumentos adicionais para passagem dos objetos do Hibernate.
Usando uma classe utilitária com o pattern Singleton e ThreadLocal
  • O código de negócio e de persistência é auto-contido, sendo facilmente reutilizado mesmo em aplicações não-web.
  • As classes de negócio e de persistência são testáveis fora da aplicação (por exemplo com o JUnit)
  • Não é possível percorrer atributos e relacionamentos com lazy loading nas classes de apresentação.
  • É necessário criar métodos “intermediários” para delimitar o início e o final de sessões e transações, antes da chamada a métodos simples de cadastro e consulta.

Tabela 2. Vantagens e desvantagens das duas soluções propostas pelo artigo para o uso do Hibernate em aplicações web.

Vistos os conceitos necessários para o uso eficiente do Hibernate em aplicações web, passamos à nossa aplicação de exemplo. Vamos usar duas classes persistentes, Sala e Reserva, apresentadas na Listagem 3 juntamente com os respectivos arquivos de mapeamento para o Hibernate. Há um elemento que pode ser novo para os leitores, chamado <version>. Ele será explicado mais adiante.

Será possível visualizar dois tipos de mapas de reserva de salas. Um referente a uma sala, apresentando todas as reservas feitas para a semana; e outro referente a uma data, mostrando todas as reservas feitas para aquela data em todas as salas. Ambos os mapas exibem as reservas em intervalos de uma hora, de 08:00 às 19:00hs, e fornecem links do tipo “Anterior | Posterior” para mudar a data ou semana. Veja a Figura 1 para um exemplo de cada mapa. Pode-se passar facilmente de um para outro mapa clicando nos nomes das salas ou dos dias da semana. (A página de boas-vindas da aplicação apenas redireciona o usuário para o mapa da data de corrente.)

Não incluímos listagens da camada de apresentação do exemplo para poupar espaço, mas os fontes completos estão disponíveis para download no site da Java Magazine. Como pode ser visto na Figura 2, temos três páginas JSP atuando como visualizações (“V” do MVC), e dois servlets atuando como controladores (“C” do MVC), um para a navegação pelos dois mapas de alocação, outro para o processo de criação, edição e cancelamento de reservas. A camada de apresentação faz uso intenso de JSTL e contém ainda três tags customizadas, descritas no quadro “Tags JSP customizados no exemplo”.

Para simplificar o exemplo, não é oferecida uma interface para o cadastramento de salas. Os registros correspondentes serão pré-inicializados no banco de dados (veja o quadro “Configurando o HSQLDB para rodar o exemplo”), então as únicas modificações possíveis na aplicação serão a criação ou a modificação de uma nova reserva, por qualquer uma das duas visualizações.

Cada reserva registra o seu “dono” e um texto descrevendo o motivo da reserva. (Não tivemos preocupação neste exemplo em realizar controle de acesso ou de outra forma impedir um usuário de modificar uma reserva feita por outro.)

A classe ReservaDAO (Listagem 3) combina as camadas de negócio e de persistência. (É comum em aplicações simples não haver necessidade de classes separadas para estas duas camadas.) Ela usa a classe utilitária, chamada de TransacaoAplicacao, que discutimos ao apresentar a segunda alternativa de arquitetura.

Como os dados de teste incluem uma reserva pré-cadastrada, o leitor pode navegar pelo mapa semanal (ou pelo diário, o que tomará mais tempo) e testar a aplicação em execução antes de prosseguir. No final do artigo são dadas instruções para compilação e execução do exemplo.

Concorrência e transações da aplicação

Já mostramos como usar o Hibernate de forma eficaz dentro de uma aplicação web. Agora veremos como lidar com transações da aplicação utilizando locks otimistas.

Para ilustrar o resultado desejado, você pode abrir duas janelas do seu navegador web e abrir em ambas um mesmo mapa de alocação (para o mesmo dia ou a mesma semana). Em uma das janelas, clique em um dos links “livre” e, antes de clicar no botão Salva, clique no mesmo link (ou seja, mesma sala, dia e hora) na outra janela. Assim você estará simulando dois usuários concorrentes tentando fazer a mesma reserva. A primeira janela em que for clicado o botão Salva deverá retornar para o mapa anterior, exibindo a posição ocupada pela reserva recém-criada. A segunda janela deverá receber uma mensagem de erro, indicando que aquela combinação de sala, data e horário não está mais disponível. A Figura 3 ilustra a seqüência de telas para cada janela nesta simulação.

Haverá um resultado semelhante se houver uma tentativa de editar a mesma reserva por dois usuários, ou então de cancelar uma reserva enquanto ela está em edição. A idéia aqui é que os registros correspondentes não são travados no banco de dados, permitindo que haja visualizações e tentativas de modificação simultâneas sobre o mesmo registro. Entretanto, se o registro for modificado entre o momento em que o usuário iniciou a edição e o momento em que clicou em Salva, a edição não pode ser efetivada e o usuário tem que ser informado de que outro usuário “chegou primeiro”.

Programadores iniciantes poderão achar esta abordagem estranha, especialmente depois que aprenderem sobre locks no banco de dados, mas há muitos bons motivos para o seu uso (veja o quadro “Locks pessimistas e otimistas”). Na verdade, a maioria dos sistemas de informação em produção adota esta abordagem. Os locks do banco de dados são utilizados apenas para o final de uma transação da aplicação, quando são salvas as alterações. Estas alterações têm que ser registradas no banco de forma “tudo ou nada”, para garantir a consistência dos dados. Então é usada uma transação do banco de dados e conseqüentemente os locks do BD.

Transações da aplicação no Hibernate

Falta mostrar como o Hibernate facilita a vida do desenvolvedor que deseja implementar locks otimistas. Já estabelecemos que o tempo de vida de um Session e o de um Transaction serão limitados a apenas uma requisição HTTP. Entretanto, conceitualmente uma transação de edição de dados da aplicação envolve duas ou mais requisições, pois o usuário tem primeiro que visualizar os dados a serem alterados, para depois enviar o formulário HTML com as alterações.

Se permitirmos que vários usuários vejam (para depois modificar) os mesmos dados, haverá modificações “perdidas” pelo banco, que poderão resultar em vários tipos de inconsistências. Por exemplo, a mesma sala de reunião poderia ser reservada várias vezes, sem que os usuários percebam que apenas a última reserva a ser gravada é a que ficaria registrada no sistema.

É possível evitar estes conflitos fazendo-se o lock dos registros no banco de dados, no momento em que são visualizados. Entretanto, como os locks só são mantidos durante a transação que os originou, isso obrigaria uma transação do banco – e conseqüentemente o Session e o Transaction do Hibernate – a ser mantida aberta por várias requisições HTTP.

Então precisamos de alguma forma de evitar conflitos, que não dependa de locks no banco de dados. A solução é usar os locks otimistas que em geral são implementados armazenando-se um serial[4] dentro do registro. Quando o registro é modificado, o serial do registro armazenado em memória é comparado com o serial no banco de dados. Se os valores forem diferentes, a modificação é abortada e o usuário tem que reiniciar a operação que estava modificando o registro, desde o ponto em que os primeiros dados são lidos do banco.

O recurso de locks otimistas é suportado pelo Hibernate, sem exigir código adicional na aplicação. Basta inserir no mapeamento da classe persistente o elemento <version>, que substitui o elemento <property> para o atributo que armazena o serial. Qualquer tentativa de atualizar um objeto que já tenha sido atualizado por outra transação gera a exceção StaleObjectStateException ("stale" = velho, obsoleto).

Um inconveniente de usar locks otimistas é que a aplicação tem que preservar o valor do serial do objeto entre a requisição de leitura e a requisição de alteração; ou então preservar o objeto inteiro na sessão HTTP, o que pode impactar a performance em clusters.

Para ver o uso de locks otimistas pela aplicação de exemplo, vamos retornar às Listagens 3 e 4 já apresentadas. A primeira demonstra o uso do elemento <version> no mapeamento da classe Reserva; a segunda ilustra a captura das exceções ConstraintViolationException[5] e StaleObjectStateException pelos métodos salvaReserva() e cancelaReserva() da classe ReservaDAO para detectar conflitos de atualização de um mesmo objeto.

Instalando e executando a aplicação de exemplo

Os fontes para download da aplicação formam um projeto web desenvolvido com o NetBeans 5.0, mas o exemplo não é em nada preso ao IDE. Para facilitar o trabalho de usuários de outros IDEs (ou dos que preferem usar a linha de comando) está incluso o script compila.bat (e compila.sh para usuários Linux) que deve ser alterado para indicar os diretórios onde estão o Hibernate, o HSQLDB, o Jakarta Commons Taglibs Standard e o Tomcat.

Resta apenas demonstrar como podemos configurar o Hibernate para utilizar um DataSource gerenciado pelo container web, no caso o Tomcat. Voltando à Figura 2, que mostra a organização de arquivos do exemplo, note que a aplicação inclui um arquivo META-INF/context.xml (mostrado na Listagem 5), que define o DataSource para o Tomcat.

Caso o deployment da aplicação seja feito pela simples cópia para a pasta webapps do Tomcat, o arquivo context.xml não será processado, e a aplicação não irá funcionar. Isso acontece porque não será localizado o DataSource configurado para o Hibernate. Há várias alternativas para resolver o problema:

  • Copiar o conteúdo do arquivo context.xml da aplicação para dentro do elemento <Host> do arquivo server.xml do Tomcat, modificando o atributo docBase para referenciar a pasta correta de instalação do exemplo.
  • Modificar o atributo docBase do context.xml, da mesma forma do primeiro item, e então copiar este arquivo para conf/Catalina/localhost/reservaSalas.xml dentro da instalação do Tomcat.
  • Fazer o deployment da aplicação empacotada em um arquivo war ConstraintViolationException>[6] por meio da aplicação Manager do Tomcat, o que garante o processamento do context.xml dentro da pasta META-INF da própria aplicação, sem necessidade de modificar qualquer arquivo ou pasta de configuração do Tomcat.

Para qualquer dessas alternativas, também será necessário copiar o driver JDBC do HSQLDB para a pasta common/lib do Tomcat. Isso porque não basta ter o driver visível no classpath da aplicação – o driver deve estar no classpath do próprio Tomcat para que possa gerenciar o pool de conexões.

Independentemente de como foi feito o deployment da aplicação, a página inicial da aplicação poderá ser acessada pela URL http://127.0.0.18080/reservaSalas.

Apresentamos duas técnicas comuns para o uso do Hibernate dentro de aplicações web, explicando as vantagens e desvantagens de cada uma. Também mostrou como lidar com locks otimistas, de modo a obter maior escalabilidade e concorrência em uma aplicação na qual vários usuários podem estar visualizando e editando simultaneamente o mesmo conjunto de objetos. E por fim juntamos tudo em uma aplicação web completa seguindo a arquitetura MVC.

Camadas em uma aplicação web

O uso da arquitetura MVC (Model-View-Controller) é hoje um padrão de mercado no desenvolvimento de aplicações web em Java, sendo adotada pela maioria dos frameworks web, como Struts, JSF e WebWork (e também por toolkits gráficos como Swing e JFace). A separação das classes de uma aplicação em três camadas –Apresentação, Negócio e Persistência – também é recomendada para a maioria dos projetos JEE, porém é importante destacar que as duas arquiteturas não são equivalentes.

A Figura Q1 apresenta a correspondência entre essas camadas e os elementos que as implementam em uma aplicação Java para web típica. Note que tanto a Visão (View) quanto o Controlador (Controller) são parte da camada de Apresentação, e que podemos considerar que o Modelo (Model) inclui tanto a camada de negócio quanto a de persistência.

Correspondência entre a arquitetura MVC e o modelo de três camadas para o desenvolvimento web em Java
Figura Q1. Correspondência entre a arquitetura MVC e o modelo de três camadas para o desenvolvimento web em Java.

Essa fusão gera um modelo de quatro camadas, mas é difícil visualizar a camada de negócio em uma aplicação simples, que consiste apenas em ler e gravar informações em um banco de dados. Não há nenhum problema em chamar nestes casos a camada de persistência diretamente do controlador, mas deve-se ter cuidado para não incluir no controlador lógica de negócio ou de persistência.

Por exemplo, se uma dada operação da aplicação envolve chamar em seqüência três métodos (possivelmente para atualizar três objetos persistentes distintos e não-relacionados), esta seqüência de operações forma conceitualmente uma única operação de negócio – que deveria estar encapsulada por um método na camada de negócio. Não criar este método, por mais simples que possa ser, significa embutir na camada de apresentação o conhecimento de negócio (por que chamar estes três métodos nesta seqüência específica?).

Nem sempre uma classe de negócio ou de persistência pode ser utilizada diretamente como parte do modelo em uma aplicação MVC. Isto depende das exigências particulares que as visualizações impõem sobre o modelo. Por exemplo, em uma aplicação Swing espera-se que uma classe de modelo gere eventos para atualização de múltiplas classes de visualização (ex.: componentes gráficos), que podem estar conectadas à mesma classe de modelo. Mas capturar esses eventos é inviável numa aplicação web, de modo que inserir esta capacidade em uma classe de negócio ou de persistência seria poluir a classe com funcionalidade necessária para a camada de apresentação.

Nesse caso, teria que ser criada uma nova classe de modelo, que faria parte da camada de apresentação, não da camada de negócios ou de persistência. Essa nova classe encapsularia a verdadeira classe de negócio ou de persistência e seria responsável por gerar os eventos necessários para atualizar a Visão (View), em colaboração estreita com o controlador. Esta colaboração é freqüentemente necessária em aplicações gráficas, por exemplo para atualizar de forma eficiente um componente JTable.

Conceitos essenciais do Hibernate

As classes e interfaces do Hibernate ficam no pacote org.hibernate e são fornecidas pelo arquivo hibernate3.jar. Este jar depende de vários outros jars, que estão presentes na pasta lib da instalação do Hibernate. As principais classes/interfaces do Hibernate são:

  • Configuration: Processa o arquivo de configuração hibernate.cfg.xml, que fornece informações, como o banco de dados a ser utilizado, e permite a geração de um SessionFactory.
  • SessionFactory: Mantém o mapeamento entre as classes persistentes e o banco de dados, e permite a criação de um Session.
  • Session: Representa um conjunto de objetos persistentes que estão instanciados em memória, e mantém o seu estado em sincronia com o banco de dados no final de uma transação. Possibilita também a criação de objetos Query.
  • Query: Permite a execução de consultas em HQL. HQL é uma linguagem semelhante ao SQL, mas que atua sobre objetos persistentes e seus atributos e relacionamentos (incluindo arrays e coleções), em vez de sobre tabelas e campos de um banco de dados relacional. Toda consulta HQL é convertida em uma consulta SQL equivalente antes de sua execução.
  • Transaction: Possibilita que o commit ou o rollback de uma transação seja realizado independentemente do ambiente de execução da aplicação e de forma coerente com a sincronização de objetos persistentes gerenciados pelo Session.

Além do arquivo de configuração principal, é necessário fornecer ao Hibernate arquivos que descrevem o mapeamento entre classes persistentes e tabelas no banco de dados. Em geral, estes arquivos são chamados NomeDaClasse.hbm.xml e são armazenados junto aos arquivos .class correspondentes. O arquivo de configuração hibernate.cfg.xml é armazenado na raiz das classes da aplicação (pasta WEB-INF/classes em uma aplicação web).

Quando violar a separação entre as camadas

O uso da segunda alternativa de arquitetura em aplicações mais simples que realizam apenas operações básicas de cadastro e consulta, acabaria provocando uma correspondência um-para-um entre os métodos de persistência e os métodos de negócio. Para evitar esta profusão de métodos que não fazem quase nada, pode ser interessante “misturar” as duas arquiteturas propostas: as classes de persistência usam a classe utilitária para obter o Session, e a classe utilitária permanece gerenciando o singleton para o SessionFactory. Mas, em vez das classes de negócio ou de persistência serem responsáveis por abrir e fechar o Session e o Transaction, um servlet controlador ou filtro chama os métodos da classe utilitária para realizar essas operações.

Dessa forma as classes de negócio ou de persistência nunca precisam se preocupar com a possibilidade de serem usadas como parte de uma transação mais ampla – o que deixa o código de cada uma bem mais limpo e direto. Não haveria mais a necessidade de passar operações simples de cadastro e consulta por uma classe intermediária que apenas iria gerenciar objetos Session e Transaction.

Essa arquitetura "híbrida" conserva a principal vantagem da primeira alternativa, que é permitir que uma página JSP obtenha atributos e objetos relacionados sem que a camada de persistência tenha explicitamente requisitado a leitura desses objetos do banco de dados. Para muitos desenvolvedores, esta transparência no acesso ao BD é a principal vantagem de se usar o Hibernate.

Apesar de violar a separação entre as camadas, a abordagem é relativamente “limpa”, pois toda a ligação entre as camadas de apresentação e de persistência fica apenas no servlet controlador ou no filtro. Páginas JSP e classes de apresentação (como os Actions do Struts) não são afetadas.

Além disso, a classe utilitária, e as classes de negócio que fazem uso dela, continuam totalmente independentes da API de Servlets. É simples o reaproveitamento dessas classes em uma aplicação não-web, por exemplo numa aplicação Swing – desde que a arquitetura da aplicação forneça um ponto central para contato entre a camada de apresentação e de negócio. Frameworks para aplicações gráficas como o Spring Rich Client, o NetBeans Platform e o Eclipse RCP fornecem tais “pontos centrais” para abertura e fechamento dos Session e Transaction.

Pools de conexões da aplicação e do container

Alguns desenvolvedores perguntam por que não usar um pool de conexões interno à aplicação, visto que o Hibernate oferece suporte nativo a várias bibliotecas de pools como a C3P0 (sf.net/projects/c3p0). Utilizar um pool interno certamente simplifica o processo de deployment, já que não seria mais necessário configurar DataSources no container web (um processo que varia muito entre servidores).

Por outro lado, um pool interno não fornece gerenciabilidade sem um grande esforço do desenvolvedor. Ele deixa para a aplicação a tarefa de reunir e fornecer informações de performance, como obter a quantidade de conexões em uso ou listar conexões que estão presas há muito tempo (indicando possíveis bugs na aplicação). Já o container web oferece uma infra-estrutura pronta para esse monitoramento e depuração, e em geral permite a modificação de parâmetros de configuração do pool, sem interromper o serviço aos usuários.

Assim sendo, a necessidade de pré-configurar o container web antes de fazer o deployment da aplicação é um preço pequeno a se pagar por toda a infra-estrutura administrativa fornecida, sem que seja necessário modificar ou recompilar a aplicação para fazer uso destes recursos.

Alguns servidores mais antigos (entre eles as primeiras versões do Tomcat) forneciam apenas o container web, sem incluir o serviço JNDI e pools de conexões JDBC. Nestes casos, embutir o pool dentro da própria aplicação era a única alternativa. Hoje a maioria dos containers web, como Tomcat e Jetty, além de todos os servidores JEE que fornecem recursos de EJB (desde os livres como JBoss e Geronimo até proprietários como WebLogic e Websphere) oferecem JNDI e pools de conexões em suas instalações padrão, então julgamos que não faz mais sentido embutir o pool dentro da aplicação.

Locks pessimistas e locks otimistas

Locks do banco de dados têm duração de uma transação, sendo liberados no commit ou rollback. Isso já os torna indesejáveis em uma aplicação web, pois uma transação de banco de dados envolvendo várias requisições HTTP não pode ser recuperada de forma confiável em um ambiente de cluster. E este problema é agravado porque uma transação em aberto não pode retornar sua conexão com o banco para o pool, prejudicando a escalabilidade da aplicação.

Além disso, transações do banco não escalam bem para uma grande quantidade de usuários simultâneos. Isso porque quando há muitos registros com lock, o BD em geral “promove” os locks de registros a locks de página ou de tabela. Assim, registros que ninguém estaria editando ficariam travados, impedindo outros usuários de trabalhar com eles. Imagine este cenário em um aeroporto lotado, com apenas um guichê atendendo aos passageiros do próximo vôo...

Outro problema de usar locks do banco de dados é que um usuário pode levar um longo tempo entre a visualização de uma informação, a realização de alterações e a confirmação destas. É realmente interessante deixar o registro travado por tanto tempo, ainda mais se considerarmos que o usuário poderá decidir cancelar a edição? Lembre que na maioria das aplicações a tela de edição de dados também cumpre o papel de tela de visualização detalhada dos registros, enquanto que uma tela de consulta exibe uma versão resumida dos dados individuais.

Os locks do BD também não correspondem bem a muitos cenários de transações de aplicações. No nosso exemplo da reserva de salas, o usuário não decide fazer uma reserva baseado nos dados de uma reserva isolada; ele faz a decisão levando em conta todo o mapa de alocação, escolhendo qual o horário e sala que seja mais conveniente. Então, para garantir que a escolha do usuário seja respeitada, usando apenas locks do banco de dados, todos os registros exibidos pelo mapa teriam que ser travados. Mas isto impediria que qualquer usuário fizesse uma reserva dentro do mesmo mapa (ou seja, no mesmo dia ou semana), até que o primeiro usuário saísse da aplicação.

Isso mostra porque a técnica de lock otimista é a preferida, e porque ela recebe este nome. A aplicação está sendo otimista, deixando os usuários visualizarem e editarem quaisquer registros – contando que na maioria dos casos serão editados registros diferentes. Em contrapartida, a aplicação tem que incluir lógica adicional para resolver os conflitos nas edições, ou seja, quando a aplicação “dá azar” e dois ou mais usuários tentam editar o mesmo registro concorrentemente.

Mas isto não quer dizer, é claro, que os locks do banco de dados sejam inúteis. São eles que garantem que uma aplicação possa modificar vários registros e, em caso de falha, desfazer todas as modificações. Locks do BD também garantem que, em caso de falha de energia, o banco será capaz de ser reinicializado em um estado consistente, sem expor aos usuários modificações feitas “pela metade”.

No final das contas, locks do banco de dados (pessimistas) e locks da aplicação (otimistas) existem para atender a necessidades diferentes. Uma aplicação real irá fazer uso dos dois, em momentos diferentes.

Tags JSP customizados no exemplo

O exemplo inclui três tags customizados, dois criados com tag files e um usando simple tags. Os fontes de cada tag foram omitidos das listagens para poupar espaço (e porque não têm relação direta com o tema do artigo), mas estão inclusos nos fontes para download no site da Java Magazine. Mas para deixar o artigo mais "auto-contido", vamos relembrar o essencial sobre as técnicas utilizadas e mostrar como os tags são usados na aplicação de exemplo.

Tag files são iguais a páginas JSP na maioria dos aspectos, porém são salvos na pasta WEB-INF/tags com a extensão .tag. Esses arquivos são referenciados na página JSP por uma diretiva que inclui o atributo tagdir="/WEB-INF/tags". (A criação de tag files foi apresentada em detalhes na Edição 24.)

No exemplo, o tag file alterna.tag (referenciado por nas páginas JSP), contém a lógica para alternar as cores de fundo das linhas da tabela HTML que representa um mapa, facilitando sua leitura pelos usuários. Já o tag file linkEdicao.tag () evita a repetição do código que ficaria dentro de cada célula em ambos os mapas. Este tag pode exibir um resumo dos dados de uma reserva, ou o link “livre” para a criação de uma nova reserva.

Com a técnica de simple tags, é criada uma classe que estende javax.servlet.jsp.tagext.SimpleTagSupport redefinindo o método doTag(). O tag é descrito em um aquivo .tld na pasta WEB-INF e na página JSP é declarado em uma diretiva , que inclui o atributo uri apontando para o arquivo .tld. (Os simple tags foram apresentados na Edição 12).

Na aplicação de exemplo, foi definido o tag , descrito por calendar.tld e implementado pela classe RollTag. Este tag permite adicionar um incremento (increment) a qualquer dos campos (atributo field) de uma data, conforme interpretada por um java.util.Calendar. O tag é usado no mapa semanal para gerar as datas de segunda a sexta-feira sem ter que recorrer a scriptlets JSP.

Configurando o HSQLDB para rodar o exemplo

Os fontes para download incluem um script sql/bancoReservas.sql, que inicializa um banco de dados HSQLB para rodar o exemplo, criando as tabelas SALA e RESERVA e configurando as restrições de integridade em cada uma. Aqui está o conteúdo deste arquivo:


create table sala (

    id identity primary key,

    nome varchar(20) not null

);

alter table sala alter column id restart with 1;

 

create table reserva (

    id identity primary key,

    versao integer default 1 not null,

    dono varchar(30) not null,

    descricao varchar(150) not null,

    inicio timestamp not null,

    sala integer not null,

    constraint fk_sala foreign key (sala) references sala(id),

    constraint slot unique (sala,inicio)

);

alter table reserva alter column id restart with 1;

 

insert into sala (nome) values ('Sala 1');

insert into sala (nome) values ('Sala 2');

insert into sala (nome) values ('Sala 3');

 

insert into reserva (dono, inicio, sala, descricao) values (

  'Fernando Lozano', '2006-01-18 10:00', 1,

  'Requerimentos do projeto Portal ACME');

Observe o uso do tipo IDENTITY do HSQLDB para as chaves primárias (identificadores) das tabelas; note também que foi alterado o valor inicial para 1 (o padrão é zero). Isto é necessário porque nas classes persistentes o atributo id foi declarado com tipo int, então o Hibernate tem que estabelecer um valor como significando “objeto sem chave”, isto é, um objeto que ainda não foi persistido no banco de dados.

Outro detalhe importante no script é a definição da constraint única slot (veja o final do comando create table reserva), de modo que não seja possível inserir duas reservas com o mesmo valor para os campos INICIO e SALA. (Note mantemos a integridade no banco de dados sem apelar para chaves primárias compostas, e sem “transferir” chaves entre tabelas relacionadas.)

Para executar o script inclua o arquivo hsqldb.jar no classpath e execute na linha de comando a classe org.hsqldb.util.DatabaseManagerSwing passando a opção -url com o valor especificado na definição do DataSource no Tomcat, por exemplo jdbc:hsqldb:file:/home/lozano/jm/reservas;shutdown=true. Usuários de Linux, não devem esquecer de colocar o argumento entre aspas (por causa do sinal de ponto-e-vírgula, que é um caractere especial para o shell).

Uma vez aberto a ferramenta DatabaseManagerSwing do HSQLDB, escolha no menu a opção File|Open Script e abra o arquivo sql/bancoReservas.sql. Em seguida, clique no botão Execute SQL. O resultado deverá ser conforme a Figura Q2 (o aspecto “diferente” da figura deve-se à captura ter sido realizada em Linux, utilizando o look-and-feel GTK do Java 5.0.)

Inicialização do banco de dados para a aplicação de exemplo
Figura Q2. Inicialização do banco de dados para a aplicação de exemplo.
Mapas de alocação de salas diário e semanal Mapas de alocação de salas diário e semanal
Figura 1. Mapas de alocação de salas diário e semanal.
Arquivos e diretórios do exemplo, conforme a janela de projetos do NetBeans. Note que as pastas “Web Pages” e “Source Packages” não existem no disco, sendo exibidas pelo IDE apenas para organizar a visualização dos arquivos
Figura 2. Arquivos e diretórios do exemplo, conforme a janela de projetos do NetBeans. Note que as pastas “Web Pages” e “Source Packages” não existem no disco, sendo exibidas pelo IDE apenas para organizar a visualização dos arquivos.
O que acontece se houver tentativas simultâneas de se reservar a mesma sala, data e horário O que acontece se houver tentativas simultâneas de se reservar a mesma sala, data e horário O que acontece se houver tentativas simultâneas de se reservar a mesma sala, data e horário
Figura 3. O que acontece se houver tentativas simultâneas de se reservar a mesma sala, data e horário.

Listagem 1. Gerenciado as classes do Hibernate utilizando recursos da API de Servlets

Listener para criar o SessionFactory


package apresentacao;

 

import java.io.*;

import javax.servlet.*;

import org.hibernate.*;

import org.hibernate.cfg.*;

 

public class SessionFactoryListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent evt) {

      ServletContext ctx = evt.getServletContext();

      try {

        SessionFactory fabrica =

          new Configuration().configure().buildSessionFactory();

        ctx.setAttribute("persistencia.fabrica", fabrica);

      }

      catch(Exception e) {

        ctx.log("Excessão ao criar SessionFactory: " + e.getMessage());

        e.printStackTrace();

      }

    }

 

    public void contextDestroyed(ServletContextEvent evt) {

      ServletContext ctx = evt.getServletContext();

      try {

        SessionFactory fabrica = (SessionFactory)

          ctx.getAttribute("persistencia.fabrica");

        fabrica.close()

      }

      catch(Exception e) {

        ctx.log("Excessão ao fechar SessionFactory: " + e.getMessage());

        e.printStackTrace();

      }

    }

}

Filtro para criar o Session e o Transaction


package apresentacao;

 

import java.io.*;

import javax.servlet.*;

import org.hibernate.*;

 

public class SessionFilter implements Filter {

  private FilterConfig filterConfig = null;

 

    public void init(FilterConfig filterConfig) {

        this.filterConfig = filterConfig;

    }

 

  public void doFilter(

      ServletRequest request, ServletResponse response,

      FilterChain chain)

      throws IOException, ServletException {

   

    ServletContext ctx = filterConfig.getServletContext();

    SessionFactory fabrica = (SessionFactory)

       ctx.getAttribute("persistencia.fabrica");

 

    Session ses = null;

    Transaction trans = null;

 

    try {

      Session ses = fabrica.openSession();

      request.setAttribute("persistencia.sessao", ses);

      Transaction trans = getSessao().beginTransaction()

      request.setAttribute("persistencia.transacao", trans);

 

      chain.doFilter(request, response);

    }

    finally {

      if (trans != null) trans.close();

      if (ses != null) ses.close();

    }

  }

}

Mapeamento do Listener e do Filtro no descritor web.xml da aplicação


<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee

        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 

 

  <listener>

    <description>ServletContextListener</description>

    <listener-class>apresentacao.SessionFactoryListener</listener-class>

  </listener>

 

  <filter>

    <filter-name>SessionFilter</filter-name>

    <filter-class>apresentacao.SessionFilter</filter-class>

  </filter>

  <filter-mapping>

    <filter-name>SessionFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>

 

  <!-- .., definições de servlets -->

</web-app>

Listagem 2. Gerenciando as classes do Hibernate com uso do pattern Singleton e ThreadLocal.


package persistencia;

 

import org.hibernate.*;

import org.hibernate.cfg.*;

 

public class TransacaoAplicacao {

  private static SessionFactory fabrica = null;

  private static ThreadLocal sessao = new ThreadLocal();

  private static ThreadLocal transacao = new ThreadLocal();

 

  public static Session getSessao() {

    try {

      if (fabrica == null)

        fabrica = new Configuration().configure().buildSessionFactory();

      if (sessao.get() == null)

        iniciaTransacao();

    }

    catch (Throwable e) {

      e.printStackTrace();

    }

    return (Session)sessao.get();

  }

 

  private static Transaction getTransacao() {

    return (Transaction)transacao.get();

  }

 

  public static void iniciaTransacao() {

    if (sessao.get() != null)

      throw new PersistenciaException("Não sei como aninhar transações");

    sessao.set(fabrica.openSession());

    transacao.set(getSessao().beginTransaction());

  }

 

  public static void fechaSessao() {

    try {

      if (getSessao() != null)

        getSessao().close();

    }

    finally {

      sessao.set(null);

      transacao.set(null);

    }

  }

 

  public static void confirma() {

    try {

      if (getTransacao() != null)

        getTransacao().commit();

    }

    finally {

      fechaSessao();

    }

  }

 

  public static void aborta() {

    try {

      if (getTransacao() != null)

        getTransacao().rollback();

    }

    finally {

      fechaSessao();

    }

  }

}

Listagem 3. Classes persistentes da aplicação de exemplo

Sala.java


package persistencia;

 

public class Sala implements java.io.Serializable {

  private int id;

  private String nome;

 

  // métodos getXXX / setXXX omitidos

}

Sala.hbm.cfg


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

 

<hibernate-mapping>

  <class name="persistencia.Sala" table="sala">

    <id name="id">

      <column name="ID" not-null="true"/>

      <generator class="identity"/>

    </id>

 

    <property name="nome">

      <column name="NOME" length="20" not-null="true"/>

    </property>

 

  </class>

</hibernate-mapping>

Reserva.java


package persistencia;

 

import java.util.Calendar;

import java.util.Date;

 

public class Reserva implements java.io.Serializable {

  private int id;

  private int versao;

  private String dono;

  private String descricao;

  private Date inicio;

  private Sala sala;

 

  public Date getDia() {

    Calendar cal = Calendar.getInstance();

    cal.setTime(inicio);

    cal.set(Calendar.HOUR, 0);

    cal.set(Calendar.MINUTE, 0);

    cal.set(Calendar.SECOND, 0);

    cal.set(Calendar.MILLISECOND, 0);

    return cal.getTime();

  }

 

  public int getDiaSemana() {

    Calendar cal = Calendar.getInstance();

    cal.setTime(inicio);

    return cal.get(Calendar.DAY_OF_WEEK);

  }

 

  public int getHora() {

    Calendar cal = Calendar.getInstance();

    cal.setTime(inicio);

    return cal.get(Calendar.HOUR_OF_DAY);

  }

 

  // métodos get/set omitidos

}

Reserva.hbm.cfg



<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

 

<hibernate-mapping>

  <class name="persistencia.Reserva" table="reserva">

    <id name="id">

      <column name="ID" not-null="true"/>

      <generator class="identity"/>

    </id>

 

    <version name="versao" column="VERSAO" />

   

    <property name="dono">

      <column name="DONO" length="30" not-null="true"/>

    </property>

 

    <property name="descricao">

      <column name="DESCRICAO" length="150" not-null="true"/>

    </property>

 

    <property name="inicio">

      <column name="INICIO" not-null="true"/>

    </property>

 

    <many-to-one name="sala" column="SALA" not-null="true"

       class="persistencia.Sala" cascade="save-update"/>

  </class>

</hibernate-mapping>

Listagem 4. Classes DAO da aplicação de exemplo, usando o Hibernate.


package persistencia;

 

import java.text.DateFormat;

import java.util.*;

import org.hibernate.*;

import org.hibernate.exception.*;

 

public class ReservaDAO {

  public Sala leSala(int id) {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      Sala sala = (Sala) ses.get(Sala.class, new Integer(id));

      TransacaoAplicacao.confirma();

      return sala;

    }

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException("Não foi possível recuperar a sala "

        + id, e);

    }

  }

 

  public List listaSalas() {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      Query query = ses.createQuery("from Sala s order by s.nome");

      List resultado = query.list();

      TransacaoAplicacao.confirma();

      return resultado;

    }

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException("Não foi possível listar as salas", e);

    }

  }

 

  public void salvaReserva(Reserva r) {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      ses.saveOrUpdate(r);

      TransacaoAplicacao.confirma();

    }

    catch (ConstraintViolationException e) {

      TransacaoAplicacao.aborta();

      DateFormat df = DateFormat.getInstance();

      throw new InsercaoDuplicadaException(

        "Outro usuário já fez uma reserva para a sala " +

          r.getSala().getNome() + " no dia " + df.format(r.getInicio()), e);

    }

    catch (StaleObjectStateException e) {

      TransacaoAplicacao.aborta();

      throw new LockOtimistaException("A reserva " + r.getId()

        + " já foi alterada por outro usuário", e);

    }   

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException("Não foi possível salvar a reserva: "

        + r.getId(), e);

    }   

  }

 

  public void cancelaReserva(Reserva r) {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      ses.delete(r);

      TransacaoAplicacao.confirma();

    }   

    catch (StaleObjectStateException e) {

      TransacaoAplicacao.aborta();

      throw new LockOtimistaException("A reserva " + r.getId()

        + " já foi alterada por outro usuário", e);

    }   

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException("Não foi possível salvar a reserva: "

        + r.getId(), e);

    }   

  }

 

  public Reserva leReserva(int id) {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      Query query = ses.createQuery(

          "from reserva r join fetch r.sala s " +

          "where r.id = :id ");

      query.setParameter("id", new Integer(id));

      List resultado = query.list();

      TransacaoAplicacao.confirma();

      if (resultado.size() == 1)

        return (Reserva)resultado.get(0);

      else

        return null;

    }

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException("Não foi possível recuperar a reserva "

         + id, e);

    }

  }

 

  public List listaReservasSala(int sala, Calendar inicio, Calendar fim) {

    return listaReservasSala(sala, inicio.getTime(), fim.getTime());

  }

 

  public List listaReservasSala(int sala, Date inicio, Date fim) {

    try {

      Session ses = TransacaoAplicacao.getSessao();

      Query query = ses.createQuery(

          "from reserva r join fetch r.sala s " +

          "where s.id = :sala " +

          "and r.inicio >= :inicio and r.inicio <= :fim " +

          "order by hour(r.inicio), dayofweek(r.inicio)");

      query.setParameter("sala", new Integer(sala));

      query.setParameter("inicio", inicio);

      query.setParameter("fim", fim);

      List resultado = query.list();

      TransacaoAplicacao.confirma();

      return resultado;

    }

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException(

        "Não foi possível listar as Reservas para a sala "

        + sala + " no período indicado", e);

    }

  }

 

  public List listaReservasDia(Date data) {

    Calendar cal = Calendar.getInstance();

    cal.setTime(data);

    cal.set(Calendar.HOUR_OF_DAY, 0);

    cal.set(Calendar.MINUTE, 0);

    cal.set(Calendar.SECOND, 0);

    data = cal.getTime();

    cal.add(Calendar.DAY_OF_MONTH, 1);

    try {

      Session ses = TransacaoAplicacao.getSessao();

      Query query = ses.createQuery(

          "from reserva r join fetch r.sala s " +

          "where r.inicio >= :inicio and r.inicio <= :fim " +

          "order by hour(r.inicio), s.nome ");

      query.setParameter("inicio", data);

      query.setParameter("fim", cal.getTime());

      List resultado = query.list();

      TransacaoAplicacao.confirma();

      return resultado;

    }

    catch (Exception e) {

      TransacaoAplicacao.aborta();

      throw new PersistenciaException(

          "Não foi possível listar as reservas do dia "

          + data, e);

    }

  }

}

Listagem 5. Configuração do Tomcat e do Hibernate para criação de DataSource a ser utilizado pelo Hibernate.

META-INF/context.xml


<?xml version="1.0" encoding="UTF-8"?>

<Context path="/reservaSalas">

 

  <!-- para o Tomcat 5.5 -->

  <Resource name="jdbc/Reservas" auth="Container"

   type="javax.sql.DataSource"

    maxActive="1" maxIdle="0" maxWait="-1"

    username="sa" password=""

    driverClassName="org.hsqldb.jdbcDriver"

    url="jdbc:hsqldb:file:/home/jm/reservas;shutdown=true"/>

        

  <!-- para o Tomcat 5.0 -->

  <!--

    <Resource name="jdbc/Reservas"

      auth="Container"

      type="javax.sql.DataSource" />

  -->

 

  <!--

  <ResourceParams name="jdbc/Reservas">

  -->

 

  <parameter>

    <name>maxActive</name>

    <value>1</value>

  </parameter>

  <parameter>

    <name>maxIdle</name>

    <value>0</value>

  </parameter>

  <parameter>

    <name>maxWait</name>

    <value>-1</value>

  </parameter>

 

  <parameter>

   <name>username</name>

   <value>sa</value>

  </parameter>

  <parameter>

   <name>password</name>

   <value></value>

  </parameter>

  <parameter>

     <name>driverClassName</name>

     <value>org.hsqldb.jdbcDriver</value>

  </parameter>

  <parameter>

    <name>url</name>

    <value>jdbc:hsqldb:file:/home/jm/reservas;shutdown=true</value>

  </parameter>

  -->

 

  <!--

  </ResourceParams>

  -->

 

</Context>

hibernate.xfg.xml


<?xml version='1.0' encoding='utf-8'?>

<!DOCTYPE hibernate-configuration PUBLIC

    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"

    "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

 

<hibernate-configuration>

  <session-factory>

    <!-- Parâmetros de conexão ao banco de dados -->

    <property name="connection.datasource">java:/comp/env/jdbc/Reservas</property>

    <property name="dialect">org.hibernate.dialect.HSQLDialect</property>

 

    <!-- Arquivos de mapeamento OO/Relacional -->

    <mapping resource="persistencia/Sala.hbm.xml"/>

    <mapping resource="persistencia/reserva.hbm.xml"/>

  </session-factory>

</hibernate-configuration>

Saiu na DevMedia!

  • Seja um mestre SQL:
    Uma verdade sobre o SQL é que ou você já usa, estuda ou um dia, acredite, vai usar. Vem com a gente aprender passo a passo como conversar com o banco de dados nesta série.

Saiba mais sobre Hibernate ;)

  • Guia de Hibernate:
    Neste Guia de Referência você encontrará cursos, devcasts e artigos que demonstram todos os recursos do Hibernate. Entre eles, é claro, como persistir dados em Java com o framework ORM mais utilizado pelos desenvolvedores.
  • Curso de Hibernate:
    Neste curso você aprenderá o que é o Hibernate, principal framework Java quando o assunto é persistência de dados em bancos de dados relacionais.
  • JPA e Hibernate: Acessando dados em aplicações Java:
    Veja neste artigo como criar uma aplicação (CRUD) com Hibernate e JPA.

Revista Java Magazine Edição 33

  • Revista Java Magazine Edição 33
  • [1] Aqui estamos sempre nos referindo à sessão do Hibernate (Session) e não à sessão HTTP (HttpSession) mantida pelo container web.
  • [2] Um Singleton é uma classe criada de forma que só possa ter uma única instância. Em geral a instância é obtida/acessada por meio de um método ou atributo estático.
  • [3] Um Facade é uma classe que fornece uma interface simplificada para um subsistema ou aplicação, em geral ocultando o uso de várias classes que devem atuar em conjunto para realizar operações mais complexas.
  • [4] Um serial é um número armazenado no registro, que aumenta de valor a cada vez que o registro é modificado.
  • [5] O Hibernate gera uma ConstraintViolationException sempre que uma constraint do banco de dados é violada, por exemplo, chave primária ou unicidade (unique)
  • [6] A Edição 9 da Java Magazine inclui um artigo detalhado sobre como construir e utilizar pacotes war. Veja também a Edição 18 para o uso da aplicação Manager do Tomcat.