O framework Hibernate é hoje largamente utilizado no mundo Java e permite o mapeamento de classes Java para tabelas em um banco de dados. Embora essa idéia pareça simples a princípio, quem já usou o framework sabe que, na prática, muitos problemas de sincronismo entre o banco e os objetos em memória podem surgir e isso normalmente pode deixar os programadores confusos.

Pré-requisitos

É assumido que o leitor já tenha um conhecimento básico do framework Hibernate, sabendo criar mapeamentos entre classes e tabelas no banco e que entenda o mecanismo de lazy loading do Hibernate. Sem esses conhecimentos, o entendimento desse artigo estará dificultado.

Motivação – Por que as instâncias têm diferentes estados?

Como o leitor já sabe, uma classe Java é mapeada no Hibernate como uma tabela no banco. Cada instância dessa classe corresponde a um registro da tabela alocado na memória. O mesmo acontece com Entity beans e outras soluções de mapeamento objeto relacional, que sempre tem estados definidos para essas instâncias de objetos mapeados.

No caso do Hibernate, existem 3 estados possíveis para cada instância, os quais apresento aqui e que serão explicados no decorrer do artigo:

  • Transiente
  • Persistente
  • Desatachado (detached)

Entender o funcionamento do Hibernate ao lidar com esses objetos é essencial para usar a ferramenta de maneira adequada e não existirem problemas de manutenibilidade posteriormente. Vamos entender o que significa cada estado.

Instâncias transientes

Instâncias transientes são as mais simples de serem entendidas, pois não há nada de especial nelas. São instâncias que existem na memória, mas não tem registros correspondentes existentes na tabela relacionada. Quando você aloca uma instância de uma classe mapeada com a keyword new do Java, você simplesmente criou uma instância transiente.

Hibernate usa Proxy

Antes de apresentar os outros dois estados, convém explicar algo sobre como o Hibernate funciona por dentro. Vamos supor que tenhamos duas classes: Pessoa e Telefone, cada uma mapeada para suas respectivas tabelas. Ambas estão relacionadas no banco e, no Java, Pessoa tem um List de Telefone, conforme as Listagens 1 e 2 (as anotações dos mapeamentos não são mostradas):


import java.util.List;

public class Pessoa {

      Long id;

      String nome;

      List<Telefone> telefones;

      public String getNome() {

            return nome;

      }

      public void setNome(String nome) {

            this.nome = nome;

      }

      public List<Telefone> getTelefones() {

            return telefones;

      }

      public void setTelefones(List<Telefone> telefones) {

            this.telefones = telefones;

      }

      public Long getId() {

            return id;

      }

      public void setId(Long id) {

            this.id = id;

      }

}
Listagem 01. Classe Pessoa.

public class Telefone {

      Long id;

      String numero;

      public String getNumero() {

            return numero;

      }

      public void setNumero(String numero) {

            this.numero = numero;

      }

      public Long getId() {

            return id;

      }

      public void setId(Long id) {

            this.id = id;

      }

}
Listagem 02. Classe Telefone.

Quando criamos uma instância da classe Pessoa com new (Pessoa pessoa = new Pessoa()) e chamamos o método pessoa.getTelefones(), o que obtemos? Null, é claro, uma vez que a classe acabou de ser criada.

Considere agora o código da listagem 3:


public class Consulta {

      public void imprimeTelefones() {

            Session session;

            /* Código para criar uma session hibernate*/

            /* session = SessionFactory. ; ... */

            Pessoa pessoa = session.load(Pessoa.class, new Long (9715));

            System.out.println("Nome da pessoa: " + pessoa.getNome());

            int i = 0;

            for (Telefone t : pessoa.getTelefones())

                  System.out.println(" Telefone " + (i++) + ":  " + t.getNumero()); 

      }

}
Listagem 03. Consulta de uma pessoa no banco.

Esse código carrega a pessoa de ID 9715 e imprime seu nome e telefones. Os detalhes de obtenção da Session foram omitidos no código.

Qual o resultado da execução desse código? Observe que a instância da classe Pessoa foi retornada pelo banco, através da chamada ao método load da Session Hibernate. O resultado, como é de se esperar, depende do número de registros, na tabela TELEFONE, relacionados com a pessoa de ID 9715.

A pergunta que agora não quer calar é: o que acontece quando usamos Lazy Loading? Se o mapeamento entre Pessoa e Telefone for do tipo lazy, somente quando o método pessoa.getTelefones() for chamado é que a consulta no banco será feita. Mas o a listagem 1 nos mostra que esse método não faz consulta nenhuma no banco, apenas retorna o valor de uma variável pré-existente. Que mágica aconteceu para que os telefones fossem retornados?

A resposta é que quando fizemos a consulta da Pessoa no banco usando o método session.load(), o Hibernate não nos retornou uma instância da classe Pessoa, como era de se esperar. O Hibernate na verdade retornou um Proxy para a classe Pessoa, ou seja, uma outra classe, com os mesmos métodos (mesma interface) que a classe Pessoa, mas que também guarda uma instância da classe real, o target.

Esses métodos verificam se a consulta no banco já foi feita ou não. Caso não tenha sido feita, ele faz a consulta e popula o valor do target. Caso contrário, ele simplesmente retorna o valor da propriedade sendo acessada. Existe um Proxy criado dinamicamente pelo Hibernate para cada classe mapeada.

Instâncias persistentes

Agora ficou fácil entender os outros tipos de instância. Quando fizemos a consulta por uma pessoa, na listagem 3, o Hibernate nos retornou uma instância que tem um registro correspondente no banco, incluindo o ID 9715 que usamos para realizar a busca. Toda vez que o Hibernate nos retorna uma instância persistente, ele retorna um Proxy para a classe mapeada, ao invés de retornar um objeto da classe real.

Como faríamos para realizar uma alteração no registro de buscamos na listagem 3? Veja a listagem 4:


public class Consulta2 {

      public void imprimeTelefones() {

            Session session;

            /* Código para criar uma session hibernate*/

            /* session = SessionFactory. ; ... */ 

            Pessoa pessoa = session.load(Pessoa.class, new Long (9715));

            System.out.println("Nome da pessoa: " + pessoa.getNome());

            pessoa.setNome("Sr. " + pessoa.getNome());

            session.update (pessoa);    

      }

}
Listagem 04. Consulta e alteração de uma pessoa no banc.

Chamamos o método update() da Session para salvar as alterações em nossa instância. Ao chamarmos esse método, um UPDATE é realizado no banco de dados. Poderíamos usar o método save() para salvar uma instância transiente e um INSERT seria realizado no banco de dados.

Instâncias desatachadas (detached)

Existe uma coisa no hibernate chamada contexto de persistência. Esse contexto é na verdade um nome mais bonito para a Session, que representa um caminho de acesso ao banco. Quando fizemos a consulta pela Pessoa na listagem 4, o Proxy retornado pelo Hibernate veio associado à Session que foi usada para realizar a consulta. Se realizássemos a consulta em outra Session, obteríamos uma outra instância associada à outra Session.

Acontece que a Session precisa de uma conexão com o banco aberta. Se nossa instância está em estado persistente, ou seja, existe na tabela e ainda está associada a uma session aberta, o que acontece quando usamos essa instância para popular valores em um JSP ou para retornar dados para um outro programa? A Session é fechada e a instância fica sem um contexto de persistência associado. Quando isso acontece, o estado do objeto deixa de ser persistente e passa a ser desatachado.

A diferença fundamental que isso acarreta é que não podemos mais usar o método update ou save para tentar salvar nossas alterações no banco, como fizemos na listagem 4. Para salvar uma instância desatachada, o Hibernate vai usar o ID para reconhecer o registro no banco e não informações associadas com a Session guardadas na memória, então usamos o método merge(), conforme ilustra a listagem 5:

Listagem 05. Merge de uma instância desatachada.

public class Consulta3 {

      public void imprimeTelefones() {

            Session session;

            /* Código para criar uma session hibernate*/

            /* session = SessionFactory. ; ... */

            // Pessoa pessoa = session.load(Pessoa.class, new Long (9715));

            Pessoa pessoa = new Pessoa();       

            pessoa.setId(new Long(9715));

            pessoa.setNome("Francisco");

            pessoa.setTelefones(null);

            System.out.println("Nome da pessoa: " + pessoa.getNome());

            pessoa.setNome("Sr. " + pessoa.getNome());

            session.merge (pessoa); 

      }

}

Perceba que ao invés de usarmos o keyword new, poderíamos ter feito uma consulta no banco, ter passado o objeto para uma JSP e então tentar salvar o objeto vindo de volta no request.

Lazy Initialization Exception

O uso de instâncias desatachadas é a principal causa de da exceção “Lazy Initialization Exception”. Ela ocorreria, por exemplo, se tentássemos chamar o método pessoa.getTelefones() numa instância desatachada, pois o Hibernate não conseguiria fazer uma consulta no banco para pegar os telefones sem que a instância esteja associada com uma sessão aberta. Para solucionar isso, poderiamos chamar o método enquanto a instância ainda é persistente ou carregar novamente a instância do banco antes de chamar esse método.

Transient Object Exception

Completando o artigo, quando ocorre essa famosa exceção? Vejamos a listagem 6:

Listagem 06. TransientException generator.

import java.util.ArrayList;

import java.util.List;

 

public class TransientExceptionGen {

      public void imprimeTelefones() {

            Session session;

            /* Código para criar uma session hibernate*/

            /* session = SessionFactory. ; ... */

            Pessoa pessoa = session.load(Pessoa.class, new Long (9715)); 

            List<Telefone> telefones = new ArrayList<Telefone>();

            Telefone telefone = new Telefone();

            //veja que telefone.getId() == null

            telefone.setNumero("1234 5678");

            pessoa.setTelefones(telefones);

            System.out.println("Nome da pessoa: " + pessoa.getNome());

            pessoa.setNome("Sr. " + pessoa.getNome());     

            session.update (pessoa);
      }
}

Veja que Pessoa agrega Telefone e, nesse caso, tentamos salvar uma instância da classe Pessoa em estado persistente que agrega uma instância de Telefone transiente. Embora seja possível mudar esse comportamento no mapeamento, a atitude padrão do Hibernate ao mandá-lo salvar pessoa no banco é tentar salvar pessoa com todas as suas agregações. Contudo, só é permitido salvar agregações transientes quando estamos salvando instâncias transientes.