Pesquisar este blog

segunda-feira, 19 de outubro de 2015

Evitando exceções ao gravar com JPA e Hibernate - um guia prático

Muitos usuários tem problemas ao gravar dados de múltiplas tabelas, neste post serão explicados como evitar este tipo de problema.

Exceção de Detached Object

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: br.unip.dsd.modelos.Usuario
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141) 
Este é um erro muito comum de acontecer com objetos java. Esta exception ocorre quando tentamos gravar um objeto que já está no banco, ou seja tem um id definido (no caso dos objetos em que o id for gerado pelo banco). Mas estamos tentando gravar ele em uma outra conexão, por exemplo você buscou o objeto no banco de dados, fechou a conexão ou transferiu ele para um outro bean, como estamos fazendo no caso da gravação do usuário aqui, usando este código:
<h:commandButton value="Continuar"
 action="#{usuarioDetalheBean.registrar(usuarioBean.usuario)}" class="btn btn-default btn-lg"
        ajax="false">
  <f:param name="includeInSalvar" value="true" />
 </h:commandButton> 
Repare que neste código  que o valor da entidade usuario, é repassada do usuarioBean para o usuarioDetalheBean. Com isto este objeto ficará desligado do banco, uma vez que o objeto que inseriu ele no banco, o usuarioBean, é diferente do objeto que irá gravá-lo novamente. Lembre-se que ao gravar um objeto relacionado com o usuário, no caso o usuário bean, o hibernate irá tentar gravar também o objeto usuário, já que este objetos são relacionados pela chave primária:
        @OneToOne
 @PrimaryKeyJoinColumn
 @MapsId
 private Usuario usuario;
Esta exceção é um tipo de proteção do hibernate, pois ele não sabe há quanto tempo este objeto foi pego do banco, e muito menos se houve alguma alteração, por isso ele dispara esta exception.
Para resolver este problema, vamos reatachar este objeto ao banco, para fazer isto muitas pessoas recomendam ou atualizar, ou fazer um merge deste objeto. Estes métodos fazem uma operação de atualização no banco de dados, mas como no nosso caso este objeto não deve ter sido alterado, então vamos buscá-lo novamente no banco, assim:

  Usuario usuarioBanco = repositorioUsuario.findOne(usuario.getId());
  usuarioDetalhe.setUsuario(usuarioBanco);
Desta forma, ao gravarmos o objeto usuário detalhe, o hibernate não irá disprar nenhuma exceção.

TransientPropertyException ao salvar relacionamentos

Uma outra exception bem comum de acontecer é a de TransientException, como mostrado abaixo:
org.hibernate.TransientPropertyValueException: object references an unsaved 
transient instance - save the transient instance before flushing
Esta exception acontece quando tentamos salvar um dado de algum relacionamento, geralemente onetomany ou manytoone, e o dado filho não está gravado.
Por exemplo, em nossa estrutura mostrada nesta figura ao salvarmos o UsuarioDetalhe, sem antes termos salvo os dados do Endereço, esta exception será disparada.
E como resolver este problema?
Existem duas formas, a primeira é alterar o tipo de cascada do relacionamento:

@Entity
public class UsuarioDetalhe {

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 @OneToOne(cascade=CascadeType.ALL)
 @PrimaryKeyJoinColumn
 private Endereco endereco;
Veja que ao setarmos o tipo de relacionamento para cascade=CascadeType.ALL todas as operações que forem realizadas no objeto pai, neste caso o UsuarioDetalhe, serão também realizadas no objeto filho. Portanto ao salvarmos o objeto UsuarioDetalhe, o objeto Endereco também será salvo.
Existem algumas ressalvas a este tipo de solução:
Este tipo de cascata é bem perigoso, pois ao configuar isso em todos os seus relacionamentos, um simples delete em uma tabela de alta hierarquia, com muitos relacionamentos filhos, podem apagar muitos dados do seu banco de dados. É bem fácil de perder o controle com este tipo de configuração.
A outra forma de evitarmos esta exception é:
@Transactional()
public void gravaDetalhe(Usuario usuario,UsuarioDetalhe usuarioDetalhe, Endereco endereco, Rua rua, Cidade cidade, 
  Estado estado, TipoLogradouro tipoLogradouro, CEP cep) {
 Usuario usuarioBanco = repositorioUsuario.findOne(usuario.getId());
 usuarioDetalhe.setUsuario(usuarioBanco);
 repositorioCidade.save(cidade);
 cep.setCidade(cidade);
 repositorioRua.save(rua);
 cep.setRua(rua);
 repositorioEstado.save(estado);
 cep.setEstado(estado);
 repositorioTipoLogradouro.save(tipoLogradouro);
 cep.setTipoLogradouro(tipoLogradouro);
 repositorioCEP.save(cep);
 endereco.setCep(cep);
 repositorioEndereco.save(endereco);
 usuarioDetalhe.setEndereco(endereco);
 repositorioUsuarioDetalhe.save(usuarioDetalhe);  
}
Aqui neste método todos os objetos são salvos em uma sequência lógica, obedecendo a ordem, primeiro salva-se os filhos, para depois salvar os objetos com hierarquia mais alta:
Usuário -> Endereço ->  Cidade, Estado, Rua e Tipo de Logradouro -> CEP
Com isto, todos os objetos já estarão salvos ao salvarmos o UsuarioDetalhe, que é o objeto pai de toda esta hierarquia.
Todas estas operações estarão dentro de uma mesma transação, garantidos pela anotação @Transaction, conforme mostrado aqui.

Nenhum comentário:

Postar um comentário