Enum persistenti in JPA

Java Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Introduzione

In JPA versione 2.0 e precedenti, non esiste un modo conveniente per mappare i valori Enum a una colonna del database. Ogni opzione ha i suoi limiti e svantaggi. Questi problemi possono essere evitati utilizzando JPA 2.1. Caratteristiche.

In questo tutorial, daremo uno sguardo alle diverse possibilità che abbiamo per rendere persistenti le enumerazioni in un database utilizzando JPA. Descriveremo anche i loro vantaggi e svantaggi, oltre a fornire semplici esempi di codice.

2. Utilizzando @Enumerated Annotazione

L'opzione più comune per mappare un valore enum da e verso la sua rappresentazione del database in JPA prima della 2.1. consiste nell'usare l' annotazione @Enumerated . In questo modo, possiamo istruire un provider JPA a convertire un'enumerazione nel suo valore ordinale o String .

Esploreremo entrambe le opzioni in questa sezione.

Ma prima, creiamo un semplice @Entity che useremo in questo tutorial:

@Entity public class Article { @Id private int id; private String title; // standard constructors, getters and setters }

2.1. Mappatura del valore ordinale

Se inseriamo l' annotazione @Enumerated (EnumType.ORDINAL) nel campo enum, JPA utilizzerà il valore Enum.ordinal () durante la persistenza di una determinata entità nel database.

Introduciamo la prima enumerazione:

public enum Status { OPEN, REVIEW, APPROVED, REJECTED; }

Successivamente, aggiungiamolo alla classe Article e annotiamolo con @Enumerated (EnumType.ORDINAL) :

@Entity public class Article { @Id private int id; private String title; @Enumerated(EnumType.ORDINAL) private Status status; }

Ora, quando si mantiene un'entità articolo :

Article article = new Article(); article.setId(1); article.setTitle("ordinal title"); article.setStatus(Status.OPEN); 

JPA attiverà la seguente istruzione 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]

Un problema con questo tipo di mappatura sorge quando dobbiamo modificare la nostra enum. Se aggiungiamo un nuovo valore al centro o riorganizziamo l'ordine dell'enumerazione, interromperemo il modello dati esistente .

Tali problemi potrebbero essere difficili da individuare e problematici da risolvere, poiché dovremmo aggiornare tutti i record del database.

2.2. Valore stringa di mappatura

Analogamente, JPA utilizzerà il valore Enum.name () durante la memorizzazione di un'entità se annotiamo il campo enum con @Enumerated (EnumType.STRING) .

Creiamo la seconda enumerazione:

public enum Type { INTERNAL, EXTERNAL; }

E aggiungiamolo alla nostra classe Article e annotiamolo con @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; }

Ora, quando si mantiene un'entità articolo :

Article article = new Article(); article.setId(2); article.setTitle("string title"); article.setType(Type.EXTERNAL);

JPA eseguirà la seguente istruzione 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]

Con @Enumerated (EnumType.STRING) , possiamo aggiungere in sicurezza nuovi valori di enumerazione o modificare l'ordine dell'enumerazione . Tuttavia, la ridenominazione di un valore enum interromperà comunque i dati del database.

Inoltre, anche se questa rappresentazione dei dati è molto più leggibile rispetto all'opzione @Enumerated (EnumType.ORDINAL) , occupa anche molto più spazio del necessario. Questo potrebbe rivelarsi un problema significativo quando dobbiamo gestire un volume elevato di dati.

3. Utilizzo delle annotazioni @PostLoad e @PrePersist

Un'altra opzione che abbiamo per gestire le enumerazioni persistenti in un database è utilizzare i metodi di callback JPA standard. Possiamo mappare le nostre enumerazioni avanti e indietro negli eventi @PostLoad e @PrePersist .

L'idea è di avere due attributi in un'entità. Il primo è mappato a un valore di database e il secondo è un campo @Transient che contiene un valore enum reale. L'attributo transitorio viene quindi utilizzato dal codice della logica aziendale.

Per comprendere meglio il concetto, creiamo un nuovo enum e usiamo il suo valore int nella logica di mappatura:

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); } }

Abbiamo anche aggiunto il metodo Priority.of () per semplificare l' ottenimento di un'istanza Priority basata sul suo valore int .

Ora, per usarlo nella nostra classe Article , dobbiamo aggiungere due attributi e implementare metodi di callback:

@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(); } } }

Ora, quando si mantiene un'entità articolo :

Article article = new Article(); article.setId(3); article.setTitle("callback title"); article.setPriority(Priority.HIGH);

JPA attiverà la seguente query 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]

Anche se questa opzione ci offre una maggiore flessibilità nella scelta della rappresentazione del valore del database rispetto alle soluzioni descritte in precedenza, non è l'ideale. Semplicemente non sembra giusto avere due attributi che rappresentano un singolo enum nell'entità. Inoltre, se utilizziamo questo tipo di mappatura, non siamo in grado di utilizzare il valore di enum nelle query JPQL.

4. Using JPA 2.1 @Converter Annotation

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. All we need to do is to create a new class that implements javax.persistence.AttributeConverter and annotate it with @Converter.

Let's see a practical example. But first, as usual, we'll create a new 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; } }

We also need to add it to the Article class:

@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; }

Now, let's create a new CategoryConverter:

@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); } }

We've set the @Converter‘s value of autoApply to true so that JPA will automatically apply the conversion logic to all mapped attributes of a Category type. Otherwise, we'd have to put the @Converter annotation directly on the entity's field.

Let's now persist an Article entity:

Article article = new Article(); article.setId(4); article.setTitle("converted title"); article.setCategory(Category.MUSIC);

Then JPA will execute the following SQL statement:

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]

As we can see, we can simply set our own rules of converting enums to a corresponding database value if we use the AttributeConverter interface. Moreover, we can safely add new enum values or change the existing ones without breaking the already persisted data.

The overall solution is simple to implement and addresses all the drawbacks of the options presented in the earlier sections.

5. Using Enums in JPQL

Let's now see how easy it is to use enums in the JPQL queries.

To find all Article entities with Category.SPORT category, we need to execute the following statement:

String jpql = "select a from Article a where a.category = com.baeldung.jpa.enums.Category.SPORT"; List articles = em.createQuery(jpql, Article.class).getResultList();

It's important to note, that in this case, we need to use a fully qualified enum name.

Of course, we're not limited to static queries. It's perfectly legal to use the named parameters:

String jpql = "select a from Article a where a.category = :category"; TypedQuery query = em.createQuery(jpql, Article.class); query.setParameter("category", Category.TECHNOLOGY); List articles = query.getResultList();

The above example presents a very convenient way to form dynamic queries.

Additionally, we don't need to use fully qualified names.

6. Conclusion

In questo tutorial, abbiamo coperto vari modi per rendere persistenti i valori enum in un database. Abbiamo presentato le opzioni che abbiamo quando si utilizza JPA nella versione 2.0 e precedenti, oltre a una nuova API disponibile in JPA 2.1 e successive.

Vale la pena notare che queste non sono le uniche possibilità per gestire gli enum in JPA. Alcuni database, come PostgreSQL, forniscono un tipo di colonna dedicato per memorizzare i valori enum. Tuttavia, tali soluzioni esulano dallo scopo di questo articolo.

Come regola generale, dovremmo sempre utilizzare l' interfaccia AttributeConverter e l' annotazione @Converter se stiamo usando JPA 2.1 o successivo.

Come al solito, tutti gli esempi di codice sono disponibili nel nostro repository GitHub.

Fondo Java

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO