Arquivos de sites

Como persistir LocalDate e LocalDateTime do Java 8 com JPA

E ai galera beleza? faz muito tempo que eu não postava nada novo, então bora com “novidade” do java 8 + JPA 😀

O Java 8 trouxe muitas grandes funcionalidades e uma das mais importantes e uma das antecipadas foi a nova API de data e hora. Haviam muitos problemas com a antiga API e não vou entrar em detalhes do porque nós precisamos de uma nova. Tenho certeza que você teve que lutar com ela com frequencia.

Todos esses problemas se foram com Java 8 \o/. A nova API de data e hora é bem desenhada, fácil de usar e (finalmente) imutável. O único problema que permanece é, que você não pode usava com JPA.

Bem, isso não totalmente verdade. Você pode usa-la, porém o JPA irá mapeá-la para BLOB ao invés de DATE ou TIMESTAMP. Que significa que a base de dados não está ciente do objeto de data e não pode aplicar nenhuma otimização para isso. E não é desse jeito que deveríamos ou gostaríamos de fazê-lo.

Porque o JPA não suporta LocalDate e LocalDateTime?

A resposta é simples, o JPA 2.1 foi liberado antes do Java 8 e a API de data e hora simplesmente não existia naquela época. Portanto a anotação @Temporal pode apenas ser aplicada a atributos do tipo java.util.Date e java.util.Calendar.

Se você deseja armazenar um atributo LocalDate em uma coluna Date ou uma LocalDateTime em uma coluna TIMESTAMP, você mesmo precisa definir o mapeamento para java.sql.Date ou java.sql.Timestamp. Graças ao conversor de atributo, uma das diversas novas funcionalidades do JPA 2.1, isso pode ser alcançado com apenas algumas linhas de código.

Nos exemplos abaixo, eu vou mostrar pra vocês como criar um conversor de atributos para LocalDate e LocalDateTime. Se você quer aprender mais sobre conversor de atributo, dê uma olhada aqui (inglês).

O exemplo

Antes de nós criar os conversores de atributo, vamos dar uma olhada na entidade de exemplo para esse post:

@Entity
public class MyEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = “id”, updatable = false, nullable = false)
private Long id;

@Column
private LocalDate date;

@Column
private LocalDateTime dateTime;

}

O conversor de atributo faz parte da especificação do JPA 2.1 e pode portanto ser usado com qualquer implementação de JPA 2.1, ex.: Hibernate ou EclipseLink. Eu usei Wildfly 8.2 com Hibernate 4.3 para os seguintes exemplos.

Convertendo LocalDate

Como você pode ver no seguinte pedaço de código, não é necessário muita coisa para criar um conversor de atributo para LocalDate.

@Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {

@Override
public Date convertToDatabaseColumn(LocalDate locDate) {
return (locDate == null ? null : Date.valueOf(locDate));
}

@Override
public LocalDate convertToEntityAttribute(Date sqlDate) {
return (sqlDate == null ? null : sqlDate.toLocalDate());
}
}

Você precisa implementar a interface AttributeConverter<LocalDate, Date> e seus 2 métodos convertToDatabaseColumn e convertToEntityAttribute. Como você pode ver nos nomes dos métodos, um deles define a conversão do tipo do atributo da entidade (LocalDate) para o tipo de coluna da base de dados (Date) e o outro é a conversão inversa. A conversão em si é simples porque java.sql.Date ja nos provê o método para fazer a conversão “de” e “para” um LocalDate.

Adicionalmente o conversor de atributo precisa ser anotado com a anotação @Converter. Devido a propriedade opcional autoApply=true, o conversor será aplicado a todos os atributos do tipo LocalDate. Dê uma olhada aqui (inglês), se você quer definir o uso de cada conversor para cada atributo individualmente.

A conversão do atributo é transparente para o desenvolvedor e o atributo LocalDate pode ser usado como qualquer outro atributo da entidade. Você pode usa-lo como parâmetro de query por exemplo.

LocalDate date = LocalDate.of(2015, 8, 11);
TypedQuery<MyEntity> query = this.em.createQuery(“SELECT e FROM MyEntity e WHERE date BETWEEN :start AND :end”, MyEntity.class);
query.setParameter(“start”, date.minusDays(2));
query.setParameter(“end”, date.plusDays(7));
MyEntity e = query.getSingleResult();

Convertendo LocalDateTime

O conversor de atributo para LocalDateTime é basicamente o mesmo. Você precisa implementar a interface attributeConverter<LocalDateTime, Timestamp> e o conversor precisa ser anotado com a anotação @Converter. Assim como o LocalDateConverter, a conversão entre LocalDateTime e um java.sql.Timestamp é feita através dos métodos de conversão do Timestamp.

@Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {

@Override
public Timestamp convertToDatabaseColumn(LocalDateTime locDateTime) {
return (locDateTime == null ? null : Timestamp.valueOf(locDateTime));
}

@Override
public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDate());
}
}

Conclusão

O JPA 2.1 foi liberado antes do Java 8 e portanto não suporta a nova API de data e hora. Se você quer usar as novas classes (do jeito certo), você mesmo precisa definir a conversão para java,sql.Date e java.sql.Timestamp. Isso pode ser feito facilmente implementando a interface AttributeConverter<EntityType, DatabaseType> e anotando a classe com a anotação @Converter(autoApply=true). Setando autoApply=true, a conversão será aplicada a todos os atributos do EntityType e nenhuma alteração na entidade é necessária.

Até onde eu sei, a próxima versão do JPA suportará a nova API de data e hora e as diferentes implementações provavelmente irão suporta-la ainda mais cedo. O Hibernate 5 por exemplo irá suporta-la como uma funcionalidade proprietária.

É isso ai galera, espero tê-los ajudado 😀

Fonte: thoughts-on-java.org – Thorben Janssen

Anúncios

TransientObjectException, LazyInitializationException e outras famosas do Hibernate

Para quem desenvolve com Hibernate, sem dúvida as exceptions que aparecem mais são a TransientObjectException (TOE), LazyInitializationException (LIE) e a PersistentObjectException (POE). Semana passada tive o prazer de ministrar um treinamento de EJB3 e JSF para o pessoal da Petrobras de 5 cidades diferentes e durante o curso várias TOEs, POEs e LIEs apareceram. Vamos ver quando cada uma ocorre.

Estou usando aqui a API do Java Persistence, mas a relação é direta com o Hibernate, já que este é o meu provider. Considere duas entidades, Autor e Livro, cada uma com atributos triviais, um id que é @GeneratedValue e uma relação @ManyToMany entre elas, sendo Livro que possui o lado mappedBy. Vamos ao código:

Autor a = new Autor();
Livro l = new Livro();
a.setLivros(Collections.singleton(l));
manager.persist(a);

Resultado:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: br.com.caelum.hibernate.testes.Livro

Essa exception não ocorreria se você tivesse chamado manager.persist(l); dentro dessa mesma transação, ou utilizasse cascade=CascadeType.PERSIST no relacionamento. Um objeto transiente, ao ser persistido, não pode se referenciar a outros objetos transientes, a não ser que haja cascade!

Agora considere:

Autor a = new Autor();
a.setId(1L);
a.setNome("paulo");
    
manager.persist(a);

Resultado:

org.hibernate.PersistentObjectException: detached entity passed to persist: br.com.caelum.hibernate.testes.Autor

O id de Autor está anotado com @GeneratedValue, quando ele percebe que há um id setado ele imagina que provavelmente esse objeto já deve existir no banco de dados (repare que ele não faz select, se você colocar um id que não existe, a mesma exception ocorrerá). persist não aceita objetos detached, apenas transient e managed. Essa exception não ocorreria se seu id não fosse @GeneratedValue, ou se você tivesse puxado esse Autor através do manager.find, por exemplo.

Vamos passar para o método merge. Porque o utilizamos tanto? Pois na web, quando recebemos os parâmetros e populamos nosso Entity para atualizá-lo, ele está detached: não foi pego através do EntityManager/Session, porém possui um id setado (o que anteriormente gerou a PersistentObjectException). O merge tem um detalhe importantíssimo: ele não vai passar o estado daquela entidade para managed (como faz o saveOrUpdate do Hibernate), e sim devolver uma versão da mesma entidade que seja managed (como fazia o velho saveOrUpdateCopy), em outras palavras, futuras mudanças na entidade passada não surtirão efeito. A outra grande diferença é que agora podemos passar como argumento um objeto transiente que já possua um id setado:

Autor a = new Autor();
a.setId(1L);
a.setNome("livro1"); 
manager.merge(a);
a.setNome("livro2");

O código irá atualizar o nome do Livro de id 1 para livro1. Repare que a mudança para livro2 não surtirá efeito, já que o merge não faz attach do objeto passado como argumento, então nesse caso ele continua detached. Uma opção seria você pegar o retorno do método merge, que é o mesmo Autor, porém agora managed. A documentação do hibernate sobre o merge parece ser melhor que a especificação.

Aqui temos de ter muito cuidado, como não puxamos esse Autor pelo entityManager, caso ele possua alguns livros no seu relacionamento, nesse merge perderíamos todas essas informações!

Parece fácil evitar todas essas exceptions, então porque eu disse que ocorreram tanto no curso? Bem, estávamos usando a JPA de dentro de um container, não standalone. Considere então um session bean de granularidade fina que esteja agindo apenas como um dao (esse não é o ideal, mas fica para os testes):

@Remote
interface SessionBeanRemote {
  void persiste(Autor a);
  void persiste(Livro l);
  Livro buscaLivroPorNome(String nome);
}

E considere o cliente:

Livro l = new Livro();
sessionBeanRemoto.persiste(l);
Autor a = new Autor();
a.setLivros(Collections.singleton(l));
manager.persiste(a);

Repare como esse código é muito parecido com o primeiro desse post, porém já persistindo o Livro anteriormente, para evitar a TransientObjectException. Mas adivinhe, aqui é lançada uma TOE! Isso ocorre porque, apesar do Livro ter sido persistido, no cliente remoto ele estará transiente, pois sua chave primária não foi populada, já que o objeto foi serializado e só no servidor se encontra uma versão desse Livro com sua respectiva chave! Se fosse no EJB 2.x, onde o Entity bean também é um componente, isso não ocorreria. Mas aqui, fora do container, o Entity bean age realmente como um valeu object: não há ligação dele com o servidor.

Para resolver isso temos alguns idiomismos, mudaríamos nosso bean remoto para:

@Remote
interface SessionBeanRemote {
  Autor persiste(Autor a);
  Livro persiste(Livro l);
  Livro buscaLivroPorNome(String nome);
}

Um tanto estranho, e na nossa implementação devolveríamos o próprio argumento:

@Stateless
class SessionBean implements SessionBeanRemote {
  @PersistentContext
  private EntityManager manager;
  public Livro persiste(Livro l) {
    manager.persist(l);
    return l;
  }
  // outros metodos
}

Dessa maneira passaríamos de volta ao cliente uma versão detached do novo livro: agora com ID! Nosso cliente ficaria:

Livro l = new Livro();
l = sessionBeanRemoto.persiste(l); // agora pegamos o retorno!
Autor a = new Autor();
a.setLivros(Collections.singleton(l));
manager.persiste(a);

Existem outras alternativas, como retornar apenas a chave primária, utilizar session beans com maior granularidade, etc.

E a LazyInitializationException que mencionei? Usando hibernate ou JPA standalone estamos cansados de saber como evitá-la: basta manter a sessão aberta durante a renderização na camada de visualização. Mas com EJB a história é outra, sendo muito mais sutil: chame o método buscaPorNome, ele vai retornar ao cliente um Livro detached. Adivinhe o que acontece ao invocar o getAutores() no cliente?

Fonte: Paulo Silveira – Caelum