Enum Bertahan di JPA

Java Top

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya

1. Perkenalan

Di JPA versi 2.0 dan yang lebih lama, tidak ada cara mudah untuk memetakan nilai Enum ke kolom database. Setiap opsi memiliki batasan dan kekurangannya. Masalah ini dapat dihindari dengan menggunakan JPA 2.1. fitur.

Dalam tutorial ini, kita akan melihat berbagai kemungkinan yang kita miliki untuk mempertahankan enum dalam database menggunakan JPA. Kami juga akan menjelaskan kelebihan dan kekurangannya serta memberikan contoh kode sederhana.

2. Menggunakan Anotasi @Enumerated

Opsi paling umum untuk memetakan nilai enum ke dan dari representasi database-nya di JPA sebelum 2.1. adalah menggunakan anotasi @Enumerated . Dengan cara ini, kita dapat menginstruksikan penyedia JPA untuk mengonversi enum menjadi nilai ordinal atau Stringnya .

Kami akan menjelajahi kedua opsi di bagian ini.

Tapi pertama-tama, mari buat @Entity sederhana yang akan kita gunakan sepanjang tutorial ini:

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

2.1. Memetakan Nilai Ordinal

Jika kita meletakkan anotasi @Enumerated (EnumType.ORDINAL) di bidang enum, JPA akan menggunakan nilai Enum.ordinal () saat mempertahankan entitas tertentu dalam database.

Mari perkenalkan enum pertama:

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

Selanjutnya, mari tambahkan ke kelas Artikel dan beri anotasi dengan @Enumerated (EnumType.ORDINAL) :

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

Sekarang, saat mempertahankan entitas Artikel :

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

JPA akan memicu pernyataan SQL berikut:

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]

Masalah dengan jenis pemetaan ini muncul ketika kita perlu mengubah enum kita. Jika kita menambahkan nilai baru di tengah atau mengatur ulang urutan enum, kita akan merusak model data yang ada .

Masalah seperti itu mungkin sulit untuk ditangkap, serta bermasalah untuk diperbaiki, karena kami harus memperbarui semua catatan database.

2.2. Memetakan Nilai String

Secara analogi, JPA akan menggunakan nilai Enum.name () saat menyimpan entitas jika kita menganotasi bidang enum dengan @Enumerated (EnumType.STRING) .

Mari buat enum kedua:

public enum Type { INTERNAL, EXTERNAL; }

Dan mari tambahkan ke kelas Artikel kita dan beri anotasi dengan @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; }

Sekarang, saat mempertahankan entitas Artikel :

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

JPA akan menjalankan pernyataan SQL berikut:

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]

Dengan @Enumerated (EnumType.STRING) , kita dapat dengan aman menambahkan nilai enum baru atau mengubah urutan enum kita. Namun, mengganti nama nilai enum masih akan merusak data database.

Selain itu, meskipun representasi data ini jauh lebih mudah dibaca dibandingkan dengan opsi @Enumerated (EnumType.ORDINAL) , ini juga menghabiskan lebih banyak ruang daripada yang diperlukan. Ini mungkin menjadi masalah yang signifikan ketika kami perlu menangani data dalam jumlah besar.

3. Menggunakan @PostLoad dan @PrePersist Penjelasan

Opsi lain yang harus kita tangani dengan enum yang ada dalam database adalah menggunakan metode callback JPA standar. Kita dapat memetakan enum kita bolak-balik di acara @PostLoad dan @PrePersist .

Idenya adalah memiliki dua atribut dalam suatu entitas. Yang pertama dipetakan ke nilai database, dan yang kedua adalah bidang @Transient yang menyimpan nilai enum nyata. Atribut transien kemudian digunakan oleh kode logika bisnis.

Untuk lebih memahami konsepnya, mari buat enum baru dan gunakan nilai intnya dalam logika pemetaan:

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

Kami juga telah menambahkan metode Priority.of () untuk memudahkan mendapatkan instance Prioritas berdasarkan nilai intnya .

Sekarang, untuk menggunakannya di kelas Article kita, kita perlu menambahkan dua atribut dan mengimplementasikan metode 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(); } } }

Sekarang, saat mempertahankan entitas Artikel :

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

JPA akan memicu kueri SQL berikut:

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]

Meskipun opsi ini memberi kami lebih banyak fleksibilitas dalam memilih representasi nilai database dibandingkan dengan solusi yang dijelaskan sebelumnya, itu tidak ideal. Rasanya tidak benar memiliki dua atribut yang mewakili satu enum dalam entitas. Selain itu, jika kami menggunakan jenis pemetaan ini, kami tidak dapat menggunakan nilai enum dalam kueri 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

Dalam tutorial ini, kita telah membahas berbagai cara untuk mempertahankan nilai enum dalam database. Kami telah menyajikan opsi yang kami miliki saat menggunakan JPA di versi 2.0 dan di bawahnya, serta API baru yang tersedia di JPA 2.1 dan di atasnya.

Perlu dicatat bahwa ini bukan satu-satunya kemungkinan untuk menangani enum di JPA. Beberapa database, seperti PostgreSQL, menyediakan jenis kolom khusus untuk menyimpan nilai enum. Namun, solusi tersebut berada di luar cakupan artikel ini.

Sebagai aturan praktis, kita harus selalu menggunakan antarmuka AttributeConverter dan anotasi @Converter jika kita menggunakan JPA 2.1 atau yang lebih baru.

Seperti biasa, semua contoh kode tersedia di repositori GitHub kami.

Jawa bawah

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya