Mapping Enum trong JPA/Hibernate

Ở các phiên bản JPA 2.0 trở về trước, để ánh xạ các enum với một cột trong database rất khó khăn khi nó không có nhiều phương thức hỗ trợ.

Tuy nhiên, bắt đầu từ JPA 2.1 đã có nhiều chức năng hơn hỗ trợ ánh xạ enum xuống database. Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu qua một số cách và phân tích các ưu và nhược điểm của từng cách này.

@Enumerated

Lựa chọn phổ biến nhất để ánh xạ một enum với một cột trong database đó là sử dụng @Enumerated annotation được giới thiệu trong phiên bản JPA 2.1. Bằng cách này, chúng ta có thể chỉ định các JPA provider chuyển đổi một enum thành string hoặc số thứ tự của nó trong tập enum được định nghĩa.

Mapping Ordinal

Nếu chúng ta chú thích @Enumerated(EnumType.ORDINAL) annotation lên các trường enum trong một entity. JPA sẽ sử dụng số thứ tự của enum làm giá trị khi thêm mới hoặc cập nhật giá giá trị của trường này.

Giả sử chúng ta có Status enum:

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

Tiếp theo chúng ta sẽ ánh xạ một Article entity class sử dụng Status enum.

package entities;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;

@Entity
public class Article {
    
    @Id
    private int id;

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;

    // getter, setter, constructor

}

Tiếp theo chúng ta sẽ triển khai thử một ví dụ sử dụng Hibernate là JPA provider mặc định trong toàn bộ bài viết này.

Bây giờ thử thêm một Article và kiểm thử

import entities.Article;
import entities.Status;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Example1 {

    public static void main(String... args) {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("enum-mapping");
        EntityManager entityManager = emf.createEntityManager();
        entityManager.getTransaction().begin();

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

        entityManager.persist(article);

        entityManager.getTransaction().commit();
        entityManager.close();
        emf.close();
    }
}

Output

jpa-enum-mapping-1

Như vậy, chúng ta có thể thấy rằng kết quả lưu giá trị cho trường Status là một giá trị số nguyên tương ứng với thứ tự của nó. Nếu Status=REJECTED thì giá trị tương ứng là 3.

Cách này có một nhược điểm rất lớn khi chúng ta chỉnh sửa thứ tự các enum sẽ khiến dữ liệu bị hư hỏng. Nếu sử dụng cách này, thì khi chỉnh sửa enum bắt buộc chúng ta phải cập nhật lại toàn bộ dữ liệu trong database tương ứng với các thay đổi trong enum.

Mapping String

Tương tự, @Enumerated(EnumType.STRING) sẽ sử chuyển đổi giá trị của enum sang string để lưu xuống database.

Đầu tiên, hãy tạo một enum thứ 2

public enum Type {
    INTERNAL, EXTERNAL
}

Sau đó, thêm Type enum vào Article class và chú thích nó với @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;
}

Bây giờ, thử triển khai một ví dụ thêm mới một Article entity xuống database với Type được chỉ định.

import entities.Article;
import entities.Status;
import entities.Type;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Example2 {
    public static void main(String... args) {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("enum-mapping");
        EntityManager entityManager = emf.createEntityManager();
        entityManager.getTransaction().begin();

        Article article = new Article();
        article.setId(1);
        article.setTitle("string title");
        article.setStatus(Status.OPEN);
        article.setType(Type.EXTERNAL);

        entityManager.persist(article);

        entityManager.getTransaction().commit();
        entityManager.close();
        emf.close();
    }
}

Output

enum-mapping-jpa-2

Với @Enumerated(EnumType.STRING), chúng ta có thể thêm enum mới hoặc thay đổi thứ tự của các enum một cách an toàn. Tuy nhiên việc chỉnh sửa các enum đã tồn tại trước đó vẫn gây ra hư hỏng dữ liệu, và bắt buộc chúng ta phải cập nhật lại toàn bộ dữ liệu tương ứng với những chỉnh sửa trên enum.

Ngoài ra, nếu so sánh với @Enumerated(EnumType.ORDINAL) thì cách này cho khả năng đọc hiểu dữ liệu cách dễ dàng hơn, và trong thực tế nó cũng được sử dụng nhiều hơn là @Enumerated(EnumType.ORDINAL).

Sử dụng @PostLoad và @PrePersist

Một cách khác mà chúng ta có thể dùng để xử lý ánh xạ các enum trong JPA dựa vào các sự kiện trong vòng đời của một entity. Trong đó 2 annotation @PostLoad và @PrePersis sẽ được gọi:

  • Sau khi một entity đã được tải – @PostLoad
  • Trước khi lưu một entity mới – @PrePersist

Ý tưởng để thực hiện là chúng ta có 2 thuộc tính trong entity, một là một thuộc tính dùng để ánh xạ xuống database, thứ 2 là một thuộc tính được chú thích với @Transient đễ giữ giá trị thật của enum. Thuộc tính @Transient này được dùng trong các business logic.

Để hiểu rõ hơn, chúng ta sẽ tiến hành tạo một enum mới

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

Hàm Priority.of() được sử dụng để lấy giá trị int tương ứng với giá trị enum đang hiện hành.

Tiếp tục chỉnh sửa Article class, thêm Priority enum, và mã code xử lý entity event.

package entities;

import javax.persistence.*;

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

Cuối cùng, thêm mới một Article entity xuống database

import entities.Article;
import entities.Priority;
import entities.Status;
import entities.Type;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Example3 {

    public static void main(String... args) {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("enum-mapping");
        EntityManager entityManager = emf.createEntityManager();
        entityManager.getTransaction().begin();

        Article article = new Article();
        article.setId(1);
        article.setTitle("entity event title");
        article.setStatus(Status.OPEN);
        article.setType(Type.EXTERNAL);
        article.setPriority(Priority.HIGH);

        entityManager.persist(article);

        entityManager.getTransaction().commit();
        entityManager.close();
        emf.close();
    }
}

Output

enum-mapping-jpa-3

Mặc dù cách này cho phép chúng ta linh hoạt hơn trong việc biểu diễn giá trị dưới cơ sở dữ liệu so với các cách trước đó, nhưng cũng có một số hạn chế. Khi chúng ta có đến 2 thuộc tính biểu diễn cho một cột trong entity. Ngoài ra, nếu chúng ta sử dụng loại ánh xạ này, chúng ta không thể sử dụng giá trị của enum trong các truy vấn JPQL. 4.

JPA @Convert annotation

Để khắc phục những hạn chế của các giải pháp được trình bày ở trên, bản phát hành JPA 2.1 đã giới thiệu một API chuẩn hóa mới có thể được sử dụng để chuyển đổi một thuộc tính thực thể thành một giá trị cơ sở dữ liệu và ngược lại.Tất cả những gì chúng ta cần làm là tạo một class mới implement từ javax.persistence.AttributeConverter và chú thích nó bằng @Convert.

Chẳng hạn với Category enum sau:

package entities;

public enum Category {
    SPORT("S"), MUSIC("M"), TECHNOLOGY("T");

    private String code;

    private Category(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

Triển khai CategoryConverter

package converters;

import entities.Category;

import javax.persistence.AttributeConverter;
import java.util.stream.Stream;

public class CategoryConverter implements AttributeConverter<Category, String> {

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

Cuối cùng, chỉnh sửa lại Article class, chú thích Category với @Convert annotation cùng giá trị là CategoryConverter class.

package entities;

import converters.CategoryConverter;

import javax.persistence.*;

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

    @Convert(converter = CategoryConverter.class)
    private Category category;

    @PostLoad
    void fillTransient() {
        if (priorityValue > 0) {
            this.priority = Priority.of(priorityValue);
        }
    }

    @PrePersist
    void fillPersistent() {
        if (priority != null) {
            this.priorityValue = priority.getPriority();
        }
    }

}

Bây giờ, thử chạy thử ví dụ

import entities.*;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Example4 {

    public static void main(String... args) {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("enum-mapping");
        EntityManager entityManager = emf.createEntityManager();
        entityManager.getTransaction().begin();

        Article article = new Article();
        article.setId(1);
        article.setTitle("entity event title");
        article.setStatus(Status.OPEN);
        article.setType(Type.EXTERNAL);
        article.setPriority(Priority.HIGH);
        article.setCategory(Category.MUSIC);

        entityManager.persist(article);

        entityManager.getTransaction().commit();
        entityManager.close();
        emf.close();
    }
}

Output

enum-mapping-jpa-4

Để truy xuất dữ liệu thông qua các trường enum trong JPQL, chúng ta có thể triển khai như sau:

String jpql = "select a from Article a where a.category = entities.Category.MUSIC";

List<Article> articles = entityManager.createQuery(jpql, Article.class)
        .getResultList();

System.out.println(articles);

Lưu ý là phải truyền cụm tên đầy đủ của enum từ package đến enum. Cách này có vẽ khá tù do vậy chúng ta có một lựa chọn khác là dùng tham số.

String jpql = "select a from Article a where a.category = :category";

TypedQuery<Article> query = entityManager.createQuery(jpql, Article.class);
query.setParameter("category", Category.MUSIC);

List<Article> articles = query.getResultList();

Kết bài

Qua bài viết này chúng ta đã tìm ra những cách ánh xạ một enum với một cột trong database cũng như cách truy vấn dữ liệu thông qua enum trong JPA. Trong đó, việc sử dụng @Convert annotation là một cách tiện lợi và linh hoạt nhất, nhưng phải nhớ rằng chỉ phiên bản JPA 2.1 trở đi nó mới được cung cấp.

Sau cùng, các bạn có thể tham khảo mã nguồn được mình công khai trên github.

Nếu vẫn chưa biết cách cấu hình một project JPA – Hibermate thì bạn có thể tham khảo bài viết sau để thực hành.

Nguồn

https://www.baeldung.com/jpa-persisting-enums-in-jpa

 

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x