Persistierende Enums in JPA

Java Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Einleitung

In JPA Version 2.0 und niedriger gibt es keine bequeme Möglichkeit, Enum-Werte einer Datenbankspalte zuzuordnen. Jede Option hat ihre Vor- und Nachteile. Diese Probleme können mit JPA 2.1 vermieden werden. Eigenschaften.

In diesem Tutorial werden wir uns die verschiedenen Möglichkeiten ansehen, wie wir Enums in einer Datenbank mit JPA beibehalten können. Wir werden auch ihre Vor- und Nachteile beschreiben sowie einfache Codebeispiele bereitstellen.

2. Verwenden von @Enumerated Annotation

Die häufigste Option zum Zuordnen eines Aufzählungswerts zu und von seiner Datenbankdarstellung in JPA vor 2.1. ist die Annotation @Enumerated zu verwenden . Auf diese Weise können wir ein JPA - Provider anweisen , eine ENUM seine Ordnungs oder zu konvertieren String Wert.

Wir werden beide Optionen in diesem Abschnitt untersuchen.

Aber zuerst erstellen wir eine einfache @Entity , die wir in diesem Tutorial verwenden werden:

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

2.1. Ordnungswert abbilden

Wenn wir die Annotation @Enumerated (EnumType.ORDINAL) in das Feld enum einfügen , verwendet JPA den Wert Enum.ordinal () , wenn eine bestimmte Entität in der Datenbank beibehalten wird .

Lassen Sie uns die erste Aufzählung vorstellen:

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

Als nächstes fügen wir es der Article- Klasse hinzu und kommentieren es mit @Enumerated (EnumType.ORDINAL) :

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

Wenn Sie nun eine Artikelentität beibehalten:

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

JPA löst die folgende SQL-Anweisung aus:

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]

Ein Problem mit dieser Art der Zuordnung tritt auf, wenn wir unsere Aufzählung ändern müssen. Wenn wir in der Mitte einen neuen Wert hinzufügen oder die Reihenfolge der Aufzählung ändern, wird das vorhandene Datenmodell beschädigt .

Solche Probleme sind möglicherweise schwer zu erkennen und problematisch zu beheben, da wir alle Datenbankeinträge aktualisieren müssten.

2.2. Zuordnungszeichenfolgenwert

Analog verwendet JPA beim Speichern einer Entität den Wert Enum.name () , wenn wir das Aufzählungsfeld mit @Enumerated (EnumType.STRING) versehen .

Erstellen wir die zweite Aufzählung:

public enum Type { INTERNAL, EXTERNAL; }

Und seien wir in unseren hinzufügen Artikel Klasse und mit Anmerkungen versehen es mit @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; }

Wenn Sie nun eine Artikelentität beibehalten:

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

JPA führt die folgende SQL-Anweisung aus:

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]

Mit @Enumerated (EnumType.STRING) können wir sicher neue Aufzählungswerte hinzufügen oder die Reihenfolge unserer Aufzählung ändern. Durch das Umbenennen eines Aufzählungswerts werden die Datenbankdaten jedoch weiterhin beschädigt.

Obwohl diese Datendarstellung im Vergleich zur Option @Enumerated (EnumType.ORDINAL) weitaus besser lesbar ist , belegt sie außerdem viel mehr Speicherplatz als erforderlich. Dies könnte sich als bedeutendes Problem herausstellen, wenn wir mit einem hohen Datenvolumen umgehen müssen.

3. Verwenden von @ PostLoad- und @ PrePersist- Anmerkungen

Eine weitere Option, mit der wir uns mit persistierenden Aufzählungen in einer Datenbank befassen müssen, ist die Verwendung von Standard-JPA-Rückrufmethoden. Wir können unsere Aufzählungen in den Ereignissen @PostLoad und @PrePersist hin und her zuordnen .

Die Idee ist, zwei Attribute in einer Entität zu haben. Der erste ist einem Datenbankwert zugeordnet, und der zweite ist ein @ Transient- Feld, das einen echten Aufzählungswert enthält. Das transiente Attribut wird dann vom Geschäftslogikcode verwendet.

Um das Konzept besser zu verstehen, erstellen wir eine neue Aufzählung und verwenden ihren int- Wert in der Zuordnungslogik:

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

Wir haben auch die Priority.of () -Methode hinzugefügt, um das Abrufen einer Priority- Instanz basierend auf ihrem int- Wert zu vereinfachen .

Um es in unserer Artikelklasse zu verwenden , müssen wir zwei Attribute hinzufügen und Rückrufmethoden implementieren:

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

Wenn Sie nun eine Artikelentität beibehalten:

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

JPA löst die folgende SQL-Abfrage aus:

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]

Obwohl diese Option uns im Vergleich zu zuvor beschriebenen Lösungen mehr Flexibilität bei der Auswahl der Darstellung des Datenbankwerts bietet, ist sie nicht ideal. Es fühlt sich einfach nicht richtig an, zwei Attribute zu haben, die eine einzelne Aufzählung in der Entität darstellen. Wenn wir diese Art der Zuordnung verwenden, können wir den Wert von enum nicht in JPQL-Abfragen verwenden.

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 diesem Tutorial haben wir verschiedene Möglichkeiten zum Beibehalten von Enum-Werten in einer Datenbank behandelt. Wir haben Optionen vorgestellt, die wir bei der Verwendung von JPA in Version 2.0 und darunter haben, sowie eine neue API, die in JPA 2.1 und höher verfügbar ist.

Es ist erwähnenswert, dass dies nicht die einzigen Möglichkeiten sind, mit Aufzählungen in JPA umzugehen. Einige Datenbanken, wie z. B. PostgreSQL, bieten einen dedizierten Spaltentyp zum Speichern von Aufzählungswerten. Solche Lösungen fallen jedoch nicht in den Geltungsbereich dieses Artikels.

Als Faustregel sollten wir immer die AttributeConverter- Schnittstelle und die @ Converter- Annotation verwenden, wenn wir JPA 2.1 oder höher verwenden.

Wie üblich sind alle Codebeispiele in unserem GitHub-Repository verfügbar.

Java unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs