Enums persistentes na JPA
1. Introdução
No JPA versão 2.0 e abaixo, não há maneira conveniente de mapear os valoresEnum para uma coluna do banco de dados. Cada opção tem suas limitações e desvantagens. Esses problemas podem ser evitados usando o JPA 2.1. características.
Neste tutorial, daremos uma olhada nas diferentes possibilidades que temos para persistir enums em um banco de dados usando JPA. Também descreveremos suas vantagens e desvantagens, bem como forneceremos exemplos de código simples.
2. Usando a anotação@Enumerated
The most common option to map an enum value to and from its database representation in JPA before 2.1. is to use the @Enumerated annotation. Dessa forma, podemos instruir um provedor JPA a converter um enum em seu valor ordinal ouString.
Exploraremos as duas opções nesta seção.
Mas primeiro, vamos criar um@Entity simples que usaremos ao longo deste tutorial:
@Entity
public class Article {
@Id
private int id;
private String title;
// standard constructors, getters and setters
}
2.1. Mapeando Valor Ordinal
Se colocarmos a anotação@Enumerated(EnumType.ORDINAL) no campo enum, a JPA usará o valorEnum.ordinal() ao persistir uma determinada entidade no banco de dados.
Vamos apresentar o primeiro enum:
public enum Status {
OPEN, REVIEW, APPROVED, REJECTED;
}
A seguir, vamos adicioná-lo à classeArticle e anotá-lo com@Enumerated(EnumType.ORDINAL):
@Entity
public class Article {
@Id
private int id;
private String title;
@Enumerated(EnumType.ORDINAL)
private Status status;
}
Agora, ao persistir uma entidadeArticle:
Article article = new Article();
article.setId(1);
article.setTitle("ordinal title");
article.setStatus(Status.OPEN);
O JPA acionará a seguinte instrução SQL:
insert
into
Article
(status, title, id)
values
(?, ?, ?)
binding parameter [1] as [INTEGER] - [0]
binding parameter [2] as [VARCHAR] - [ordinal title]
binding parameter [3] as [INTEGER] - [1]
Um problema com esse tipo de mapeamento surge quando precisamos modificar nossa enumeração. If we add a new value in the middle or rearrange the enum’s order, we’ll break the existing data model.
Esses problemas podem ser difíceis de entender e problemáticos de corrigir, pois precisaríamos atualizar todos os registros do banco de dados.
2.2. Valor da cadeia de mapeamento
De forma análoga, JPA usará o valorEnum.name() ao armazenar uma entidade se anotarmos o campo enum com@Enumerated(EnumType.STRING).
Vamos criar o segundo enum:
public enum Type {
INTERNAL, EXTERNAL;
}
E vamos adicioná-lo à nossa classeArticle e anotar com@Enumerated(EnumType.STRING):
@Entity
public class Article {
@Id
private int id;
private String title;
@Enumerated(EnumType.ORDINAL)
private Status status;
@Enumerated(EnumType.STRING)
private Type type;
}
Agora, ao persistir uma entidadeArticle:
Article article = new Article();
article.setId(2);
article.setTitle("string title");
article.setType(Type.EXTERNAL);
O JPA executará a seguinte instrução SQL:
insert
into
Article
(status, title, type, id)
values
(?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [null]
binding parameter [2] as [VARCHAR] - [string title]
binding parameter [3] as [VARCHAR] - [EXTERNAL]
binding parameter [4] as [INTEGER] - [2]
Com@Enumerated(EnumType.STRING), podemos adicionar com segurança novos valores de enum ou alterar a ordem de nossa enum. However, renaming an enum value will still break the database data.
Além disso, embora essa representação de dados seja muito mais legível em comparação com a opção@Enumerated(EnumType.ORDINAL), ela também consome muito mais espaço do que o necessário. Isso pode se tornar um problema significativo quando precisamos lidar com um alto volume de dados.
3. Usando anotações@PostLoade@PrePersist
Outra opção que temos de lidar com enumerações persistentes em um banco de dados é usar métodos padrão de retorno de chamada JPA. We can map our enums back and forth in the @PostLoad and @PrePersist events.
A ideia é ter dois atributos em uma entidade. O primeiro é mapeado para um valor de banco de dados e o segundo é um campo@Transient que contém um valor enum real. O atributo transitório é então usado pelo código da lógica de negócios.
Para entender melhor o conceito, vamos criar um novo enum e usar seu valorint na lógica de mapeamento:
public enum Priority {
LOW(100), MEDIUM(200), HIGH(300);
private int priority;
private Priority(int priority) {
this.priority = priority;
}
public int getPriority() {
return priority;
}
public static Priority of(int priority) {
return Stream.of(Priority.values())
.filter(p -> p.getPriority() == priority)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
Também adicionamos o métodoPriority.of() para facilitar a obtenção de uma instânciaPriority com base em seu valorint.
Agora, para usá-lo em nossa classeArticle, precisamos adicionar dois atributos e implementar métodos de retorno de chamada:
@Entity
public class Article {
@Id
private int id;
private String title;
@Enumerated(EnumType.ORDINAL)
private Status status;
@Enumerated(EnumType.STRING)
private Type type;
@Basic
private int priorityValue;
@Transient
private Priority priority;
@PostLoad
void fillTransient() {
if (priorityValue > 0) {
this.priority = Priority.of(priorityValue);
}
}
@PrePersist
void fillPersistent() {
if (priority != null) {
this.priorityValue = priority.getPriority();
}
}
}
Agora, ao persistir uma entidadeArticle:
Article article = new Article();
article.setId(3);
article.setTitle("callback title");
article.setPriority(Priority.HIGH);
O JPA acionará a seguinte consulta SQL:
insert
into
Article
(priorityValue, status, title, type, id)
values
(?, ?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [300]
binding parameter [2] as [INTEGER] - [null]
binding parameter [3] as [VARCHAR] - [callback title]
binding parameter [4] as [VARCHAR] - [null]
binding parameter [5] as [INTEGER] - [3]
Mesmo que esta opção nos dê mais flexibilidade na escolha da representação do valor do banco de dados em comparação com as soluções descritas anteriormente, não é o ideal. Simplesmente não parece certo ter dois atributos representando um único enum na entidade. Additionally, if we use this type of mapping, we aren’t able to use enum’s value in JPQL queries.
4. Usando JPA 2.1@Converter Anotação
To overcome the limitations of the solutions shown above, JPA 2.1 release introduced a new standardized API that can be used to convert an entity attribute to a database value and vice versa. Tudo o que precisamos fazer é criar uma nova classe que implementejavax.persistence.AttributeConvertere anotá-la com@Converter.
Vamos ver um exemplo prático. Mas primeiro, como de costume, vamos criar um novo enum:
public enum Category {
SPORT("S"), MUSIC("M"), TECHNOLOGY("T");
private String code;
private Category(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
Também precisamos adicioná-lo à classeArticle:
@Entity
public class Article {
@Id
private int id;
private String title;
@Enumerated(EnumType.ORDINAL)
private Status status;
@Enumerated(EnumType.STRING)
private Type type;
@Basic
private int priorityValue;
@Transient
private Priority priority;
private Category category;
}
Agora, vamos criar um novoCategoryConverter:
@Converter(autoApply = true)
public class CategoryConverter implements AttributeConverter {
@Override
public String convertToDatabaseColumn(Category category) {
if (category == null) {
return null;
}
return category.getCode();
}
@Override
public Category convertToEntityAttribute(String code) {
if (code == null) {
return null;
}
return Stream.of(Category.values())
.filter(c -> c.getCode().equals(code))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
Definimos o valor de@Converter deautoApply paratrue para que o JPA aplique automaticamente a lógica de conversão a todos os atributos mapeados de um tipoCategory. Caso contrário, teríamos que colocar a anotação@Converter diretamente no campo da entidade.
Vamos agora persistir uma entidadeArticle:
Article article = new Article();
article.setId(4);
article.setTitle("converted title");
article.setCategory(Category.MUSIC);
O JPA executará a seguinte instrução SQL:
insert
into
Article
(category, priorityValue, status, title, type, id)
values
(?, ?, ?, ?, ?, ?)
Converted value on binding : MUSIC -> M
binding parameter [1] as [VARCHAR] - [M]
binding parameter [2] as [INTEGER] - [0]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [converted title]
binding parameter [5] as [VARCHAR] - [null]
binding parameter [6] as [INTEGER] - [4]
Como podemos ver, podemos simplesmente definir nossas próprias regras de conversão de enums em um valor de banco de dados correspondente se usarmos a interfaceAttributeConverter. Moreover, we can safely add new enum values or change the existing ones without breaking the already persisted data.
A solução geral é simples de implementar e aborda todas as desvantagens das opções apresentadas nas seções anteriores.
5. Conclusão
Neste tutorial, cobrimos várias maneiras de persistir valores enum em um banco de dados. Apresentamos as opções que temos ao usar JPA na versão 2.0 e inferior, bem como uma nova API disponível em JPA 2.1 e superior.
É importante notar que essas não são as únicas possibilidades de lidar com enums no JPA. Some databases, like PostgreSQL, provide a dedicated column type to store enum values. No entanto, tais soluções estão fora do escopo deste artigo.
Como regra geral, devemos sempre usar a interfaceAttributeConverter e a anotação@Converter se estivermos usando JPA 2.1 ou posterior.
Como de costume, todos os exemplos de código estão disponíveis em nossoGitHub repository.