Học viên: Vũ Bá Phúc
Lớp: Java Fullstack 15
Email: phucnhan20022022@gmail.com
Số điện thoại: 0968616076
Nguồn tham khảo : https://www.baeldung.com

1. Giới thiệu

Trong phiên bản JPA 2.0 trở xuống, không có cách thuận tiện nào để ánh xạ các giá trị Enum vào cột cơ sở dữ liệu. Mỗi tùy chọn đều có những hạn chế và nhược điểm của nó. Những vấn đề này có thể tránh được bằng cách sử dụng các tính năng của JPA 2.1.

Trong hướng dẫn này, chúng ta sẽ xem xét các khả năng khác nhau mà chúng ta có để duy trì các enum trong cơ sở dữ liệu bằng cách sử dụng JPA. Chúng tôi cũng sẽ mô tả các ưu điểm và nhược điểm của chúng cũng như cung cấp các ví dụ mã đơn giản.

2. Sử dụng chú thích @Enumerated

Cách phổ biến nhất để ánh xạ một giá trị enum đến cơ sở dữ liệu của nó trong JPA trước 2.1 là sử dụng chú thích @Enumerated . Bằng cách này, chúng ta có thể hướng dẫn một nhà cung cấp JPA chuyển đổi một enum thành giá trị String hoặc Ordinal của nó .

Chúng ta sẽ khám phá cả hai trong phần này.

Nhưng trước tiên hãy tạo một @Entity đơn giản mà chúng ta sẽ sử dụng trong suốt hướng dẫn này:

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

    private String title;

    // standard constructors, getters and setters
}

2.1 Ánh xạ giá trị Ordinal

Nếu chúng ta đặt chú thích @Enumerated(EnumType.ORDINAL) trên trường enum, JPA sẽ sử dụng giá trị Enum.ordinal() khi duy trì một Entity nhất định trong cơ sở dữ liệu.

Đây là enum đầu tiên:

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

Tiếp theo, hãy thêm nó vào lớp Article và chú thích nó bằng @Enumerated(EnumType.ORDINAL) :

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

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;
}

Bây giờ hãy tạo một Entity Article :

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

JPA sẽ kích hoạt câu lệnh SQL sau:

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]

Một vấn đề phát sinh với loại ánh xạ này khi chúng ta cần sửa đổi enum của mình. Nếu chúng ta thêm một giá trị mới vào giữa hoặc sắp xếp lại thứ tự của enum, chúng ta sẽ phá vỡ cấu trúc dữ liệu hiện có.

Những vấn đề như vậy có thể khó phát hiện cũng như khó khắc phục vì chúng ta sẽ phải cập nhật tất cả các bản ghi trong cơ sở dữ liệu.

2.2 Ánh xạ giá trị String

Tương tự, JPA sẽ sử dụng giá trị Enum.name() khi lưu trữ một Entity nếu chúng ta chú thích trường enum với @Enumerated(EnumType.STRING).

Hãy tạo enum thứ hai:

public enum Type {
    INTERNAL, EXTERNAL;
}

Và hãy thêm nó vào lớp Article của chúng ta và chú thích nó bằng @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ờ hãy tạo một Entity Article :

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

JPA sẽ thực thi câu lệnh SQL sau:

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]

Với @Enumerated(EnumType.STRING) , chúng ta có thể thêm các giá trị enum mới hoặc thay đổi thứ tự của enum một cách an toàn. Tuy nhiên, đổi tên một giá trị enum vẫn sẽ phá vỡ cơ sở dữ liệu ban đầu.

Ngoài ra, mặc dù cấu trúc dữ liệu này dễ đọc hơn nhiều so với việc sử dụng @Enumerated(EnumType.ORDINAL) , nó cũng tiêu tốn nhiều dung lượng hơn mức cần thiết. Điều này có thể trở thành một vấn đề quan trọng khi chúng ta cần xử lý một lượng lớn dữ liệu.

3. Sử dụng Chú thích @PostLoad@PrePersist

Một cách khác mà chúng ta làm việc với các enum tồn tại trong cơ sở dữ liệu là sử dụng các phương thức gọi lại JPA tiêu chuẩn. Chúng tôi có thể ánh xạ tới, lui các enum của mình trong các sự kiện @PostLoad@PrePersist .

Ý tưởng là có hai thuộc tính trong một Entity. Cái đầu tiên được ánh xạ tới một giá trị cơ sở dữ liệu và cái thứ hai là trường @Transient chứa giá trị enum thực. Sau đó, thuộc tính tạm thời được sử dụng bởi mã logic nghiệp vụ.

Để hiểu rõ hơn về khái niệm này, hãy tạo một enum mới và sử dụng giá trị int của nó trong logic ánh xạ:

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

Chúng ta đã thêm phương thức Priority.of() để giúp dễ dàng lấy một phiên bản Priority dựa trên giá trị int của nó .

Bây giờ, để sử dụng nó trong lớp Article của chúng ta , chúng ta cần thêm hai thuộc tính và triển khai các phương thức gọi lại:

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

Bây giờ hãy tạo một Entity Article :

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

JPA sẽ kích hoạt truy vấn SQL sau:

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]

Mặc dù cách này giúp chúng ta linh hoạt hơn trong việc chọn biểu diễn của giá trị cơ sở dữ liệu so với các giải pháp được mô tả trước đó, nhưng tùy chọn này không lý tưởng. Thật không đúng khi có hai thuộc tính đại diện cho một enum trong thực thể. Ngoài ra, nếu chúng ta sử dụng loại ánh xạ này thì chúng ta không thể sử dụng giá trị của enum trong các truy vấn JPQL.

4. Sử dụng Chú thích JPA 2.1 @Converter

Để khắc phục những hạn chế của các giải pháp nêu 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 thuộc tính thực thể thành 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 lớp mới triển khai javax.persistence.AttributeConverter và chú thích nó bằng @Converter .

Hãy xem một ví dụ thực tế.

Đầu tiên, chúng ta sẽ tạo một enum mới:

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

    private String code;

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

    public String getCode() {
        return code;
    }
}

Chúng ta cũng cần thêm nó vào lớp Article :

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

Bây giờ hãy tạo một CategoryConverter mới :

@Converter(autoApply = true)
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);
    }
}

Chúng ta đã đặt giá trị autoApply của @Converter thành true để JPA sẽ tự động áp dụng logic chuyển đổi cho tất cả các thuộc tính được ánh xạ của một loại Category . Nếu không, chúng tôi phải đặt chú thích @Converter trực tiếp vào trường của Entity.

Bây giờ chúng ta hãy tạo một Entity Article :

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

Sau đó, JPA sẽ thực thi câu lệnh SQL sau:

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]

Như các bạn có thể thấy, chúng ta có thể chỉ cần đặt quy tắc chuyển đổi enum của riêng mình thành giá trị cơ sở dữ liệu tương ứng nếu chúng ta sử dụng giao diện AttributeConverter . Hơn nữa, chúng ta có thể thêm các giá trị enum mới một cách an toàn hoặc thay đổi các giá trị hiện có mà không làm hỏng dữ liệu đã tồn tại.

Giải pháp tổng thể rất đơn giản để thực hiện và giải quyết tất cả các nhược điểm của các tùy chọn được trình bày trong các phần trước.

5. Sử dụng Enums trong JPQL

Bây giờ chúng ta hãy xem việc sử dụng enums trong các truy vấn JPQL dễ dàng như thế nào.

Để tìm tất cả các Entity Article có category là Category.SPORT , chúng ta cần thực hiện câu lệnh sau:

String jpql = "select a from Article a where a.category = com.baeldung.jpa.enums.Category.SPORT";

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

Điều quan trọng cần lưu ý là chúng ta cần sử dụng tên enum đủ điều kiện trong trường hợp này.

Tất nhiên, chúng tôi không giới hạn các truy vấn tĩnh.

Việc sử dụng các tham số được đặt tên là hoàn toàn được:

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

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

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

Ví dụ này trình bày một cách rất dễ dàng để tạo các truy vấn động.

Ngoài ra, chúng tôi không cần sử dụng tên đủ điều kiện.

6. Kết luận

Trong bài viết này, chúng ta đã đề cập đến nhiều cách khác nhau để tạo giá trị enum trong cơ sở dữ liệu. Chúng tôi đã trình bày các cách của mình khi sử dụng JPA trong phiên bản 2.0 trở xuống cũng như một API mới có sẵn trong JPA 2.1 trở lên.

Điều đáng chú ý là đây không phải là khả năng duy nhất để xử lý các enum trong JPA. Một số cơ sở dữ liệu, như PostgreSQL, cung cấp một loại cột chuyên dụng để lưu trữ các giá trị enum. Tuy nhiên, các giải pháp như vậy nằm ngoài phạm vi của bài viết này.

Theo nguyên tắc thông thường, chúng ta phải luôn sử dụng giao diện AttributeConverter và chú thích @Converter nếu chúng ta đang sử dụng JPA 2.1 trở lên.