domingo, 26 de julho de 2015

JPA AttributeConverter, ou como utilizar a nova API de data do Java junto com JPA 2.1


Uma das melhores novidades do Java 8 foi sua API de data. Muita gente critica a API de data do Java com razão e com isso muita gente passou a utilizar a JODA Time.

A nova API de data foi baseada no JODA Time e escrita com linguagem fluente, melhorando muito o entendimento e uso.

Infelizmente o JPA ainda não suporta nativamente esse tipo de data, mas uma novidade na especificação do 2.1 permite seu uso de maneira bastante simples.

AttributeConverter é uma funcionalidade muito importante que já existia no Hhibernate desde o JPA 1.0. Com ele podemos criar mapeamentos de de-para entre tipos de nossa entidade e banco de dados.

Isto é muito útil nos cenários abaixo:

  • Banco de dados com campo verdadeiro/falso mapeado com 1/0: Podemos fazer um converter e tratar na entidade como true e false.
  • Melhor tratamento mapa mapeamento de Enums: O tratamento de mapeamento de Enum é bem útil, mas não contempla todas as necessidades.
  • Casos em que uma informação complexa é gravada como string no banco de dados.

Por padrão se fizermos um mapeamento com a API de data do java 8, o dado será gravado serializado.

Para esse post fiz um projeto de exemplo com algumas entidades e vários mapeamentos para os tipos novos de data. Para ajudar fiz uma classe util que lista todas as tabelas e campos da base.

Desta maneira fica fácil ver como cada configuração afeta como a base é gerada via JPA.

Uma das entidades é Aluno e rodando o teste sem criar o AttributeConverter para LocalDate, podemos perceber que o campo foi gerado como binário.

@Entity
public class Aluno {
	
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Integer id;
	
	@Column(length=200, nullable=false)
	private String nome;
	
	@Column(nullable=false)
	private LocalDate dataNascimento;

Saída teste:

ALUNO
COLUMN_NAME=ID, DATA_TYPE=4, TYPE_NAME=INTEGER, COLUMN_SIZE=10
COLUMN_NAME=DATANASCIMENTO, DATA_TYPE=-3, TYPE_NAME=VARBINARY, COLUMN_SIZE=255

Para criar um atribute converter basta implementar a interface javax.persistence.AttributeConverter e realizar uma configuração via a anotação javax.persistence.Converter:

import java.sql.Date;
import java.time.LocalDate;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

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

	public Date convertToDatabaseColumn(LocalDate attribute) {
		return Date.valueOf(attribute);
	}

	public LocalDate convertToEntityAttribute(Date dbData) {
		return dbData.toLocalDate();
	}

}

O autoApply=true serve para indicar que por padrão a implementação JPA deve realizar essa conversão.

Caso seja colocado false, é preciso usar a anotação Convert no mapeamento do campo:

        @Convert(converter=LocalDateAtributeConverter.class)
	private LocalDate dataNascimento;

A anotação convert possui precedência sobre o converter global, então pode ser utilizado para sobrescrever alguma configuração.

Depois de feita uma das mudanças acima, a tabela é gerada corretamente:

ALUNO
  COLUMN_NAME=ID, DATA_TYPE=4, TYPE_NAME=INTEGER, COLUMN_SIZE=10 COLUMN_NAME=DATANASCIMENTO, DATA_TYPE=91, TYPE_NAME=DATE, COLUMN_SIZE=8

Para o mapeamento de Enums, por padrão (@Enumerated) podemos mapear pela ordem deles (começando com 0) ou pelo nome. Ambos os casos podem ser uteis em determinadas situações, como no enum abaixo:

public enum TipoPessoa {
	PF, PJ
}

Neste caso, não temos problema em fazer o mapeamento por nome, pois fica claro que PF e PJ se referem a pessoa física e jurídica.

Em alguns casos não podemos fazer isso, seja porque não ficaria muito legível ou seja porque estamos trabalhando com uma base legada que possui seus códigos.

Vamos imaginar que para diferenciar entrada e saída, são utilizados os códigos '001' e '002', neste caso não teríamos como fazer o mapeamento utilizando a anotação @Enumerated, mas fica bem simples utilizando o converter:

        //mapeamento
	@Column(length=3)
	private TipoPonto tipo;


public enum TipoPonto {
	ENTRADA("001"), SAIDA("002");
	
	private String codigo;
	
	private TipoPonto(String codigo) {
		this.codigo = codigo;
	}

	public String getCodigo() {
		return codigo;
	}
	
	public static TipoPonto getTipoPonto(String codigo) {
		TipoPonto[] values = values();
		for (TipoPonto tipoPonto : values) {
			if (tipoPonto.getCodigo().equals(codigo)) {
				return tipoPonto;
			}
		}
		throw new IllegalArgumentException("TipoPonto não encontrado");
	}
}

@Converter(autoApply=true)
public class TipoPontoAtributeConverter implements AttributeConverter<TipoPonto, String> {

	public String convertToDatabaseColumn(TipoPonto attribute) {
		return attribute.getCodigo();
	}

	public TipoPonto convertToEntityAttribute(String dbData) {
		return TipoPonto.getTipoPonto(dbData);
	}

}

Eu fiz outros exemplos, mas todos seguem a mesma linha, então fica mais interessante brincar com o projeto de teste que eu estou disponibilizando.

O Projeto utiliza o Maven e configurei o Hibernate como provider JPA. Como SGBD utilizei o banco H2, trabalhando apenas em memória.

O download pode ser feito aqui. Em breve vou jogar esse fonte no github.