Sử dụng Enum trong JPA

Sử dụng Enum trong JPA

Enum luôn được khuyên dùng và được sử dụng rất phổ biến trong JPA. Enum giúp kiểm tra giá trị đầu vào của database, đồng thời thể hiện trực quan giá trị của database thông qua Enum ở tầng Java code. Cùng xem qua các cách khai báo Enum và những chú ý khi sử dụng chúng trong bài viết này

Có 2 cách chính sử dụng Enum trong JPA: @Enumerated và @Convert

@Enumerated

public enum Status {
    ACTIVE, DEACTIVATE;
}
@Entity
public class Admin {
  @Id
  private int id;
  private String name;

  @Enumerated
  private Status status;
}

Mặc định Enumerated sẽ có EnumType là ORDINAL, lưu theo thứ tự khai báo của Enum và kiểu dữ liệu là Integer (hoặc NUMBER) ACTIVE sẽ là 0, và DEACTIVATE sẽ là 1

var admin = new Admin();
admin.setName("John");
admin.setStatus(Status.DEACTIVATE);
insert 
into
    Admin
    (status, name, id) 
values
    (?, ?, ?)
...
binding parameter [1] as [INTEGER] - [1]
binding parameter [2] as [VARCHAR] - [John]
binding parameter [3] as [INTEGER] - [1]

ngược lại khi query

select
        admin0_.id as id1_6_,
        admin0_.name as name2_6_,
        admin0_.status as status3_6_
    from
        admin admin0_ 
...
extracted value ([id1_6_] : [INTEGER]) - [1]
extracted value ([name2_6_] : [VARCHAR]) - [John]
extracted value ([status3_6_] : [INTEGER]) - [DEACTIVATE]

Nếu để EnumType là STRING, giá trị trong database sẽ là tên của Enum và kiểu dữ liệu là text (hoặc VARCHAR)

@Entity
public class Admin {
  @Id
  private int id;
  private String name;

  @Enumerated(EnumType.STRING)
  private Status status;
}
insert 
into
    Admin
    (status, name, id) 
values
    (?, ?, ?)
...
binding parameter [1] as [INTEGER] - [1]
binding parameter [2] as [VARCHAR] - [John]
binding parameter [3] as [VARCHAR] - [DEACTIVATE]

@Convert

Cách này yêu cầu viết thêm một Converter class như dưới đây

@Converter(autoApply = true)
public class StatusConverter implements AttributeConverter<Status, Integer> {

 public Integer convertToDatabaseColumn(Status value) {
  if (value == null) {
   return null;
  }
  return value.getValue();
 }

 public Status convertToEntityAttribute(Integer value) {
  if (value == null) {
   // Can throw an exception here
   return null;
  }
  return Status.fromValue(value);
 }
}
@Entity
public class Admin {
  @Id
  private int id;
  private String name;

  @Convert(converter = StatusConverter.class)
  private Status status;
}

Annotation @Convert ở entity có thể bỏ do ta đã khai báo @Converter(autoApply = true) Với cả 2 cách trên, khi thêm data vào database, chỉ thao tác với Enum thuần túy, không cần quan tâm giá trị thực trong database

var admin = new Admin();
...
admin.setStatus(Status.ACTIVE);

Khi query

List<Admin> findByStatus(Status status);

@Query("FROM Admin WHERE status = ?1")
List<Admin> listAllByStatus(Status status);

Với JPQL hay HQL, một khi đã sử dụng kiểu Enum, ta sẽ bắt buộc sử dụng Enum trong tham số, không thể dùng giá trị thực trong database. Không thể viết

@Query("FROM Admin WHERE status = 0")
List<Admin> listAllActiveStatus();

@Query("FROM Admin WHERE status = ?1")
List<Admin> listAllByStatus(String status); // Kiểu của status phải là Enum

Ngoài ra, bạn có thể sử dụng Custom Type. Tuy nhiên cách này phức tạp hơn và thừa thãi nếu chỉ dùng để mapping Enum. Có thể tham khảo thêm tại đây

Đánh giá các cách dùng Enum

Sử dụng Enumerated ORDINAL có vẻ ổn, nhưng sẽ phát sinh lỗi không ngờ tới nếu bạn thay đổi thứ tự của Enum

public enum Status {
    ACTIVE, BLOCKED, DEACTIVATE;
}

Lúc này nếu status trong database là 1 sẽ trả về BLOCKED chứ không còn là DEACTIVATE như cũ

select
        admin0_.id as id1_6_,
        admin0_.name as name2_6_,
        admin0_.status as status3_6_
    from
        admin admin0_ 
...
extracted value ([id1_6_] : [INTEGER]) - [1]
extracted value ([name2_6_] : [VARCHAR]) - [John]
extracted value ([status3_6_] : [INTEGER]) - [BLOCKED]

Tương tự thì với Enumerated STRING, lỗi sẽ phát sinh nếu bạn thay đổi tên các phần tử của Enum. Dùng @Convert an toàn nhưng lại yêu cầu viết thêm class Converter

Nên sử dụng Enum như thế nào?

Trong gần 10 năm qua, mình luôn luôn sử dụng kiểu số trong database sau đó mapping với JPA bằng @Convert, vì cho rằng điều đó sẽ giúp tối ưu lưu trữ trong database. Nhưng hóa ra, điều đó là không cần thiết và rất bất tiện.
Giả sử bạn sử dụng kiểu số. Dù bạn chính là người viết ra những Enum đó, thì cũng sẽ cũng lúc bạn tự hỏi những status 1, 2, 3, 4 trong database là gì. Kết quả là bạn lại phải quay lại code để xem trong Enum. Ngoài lập trình viên, nhiều role khác cũng sẽ có nhu cầu truy vấn database. Bạn sẽ lại phải giải thích cho họ status 1, 2, 3, 4 trong database có nghĩa là gì. Ngoài ra, dữ liệu khi được trích xuất và sử dụng ở những ứng dụng khác, Enum trở lên vô dụng ở đó và lại cần phải xây dựng một converter riêng.
Thay vào đó, nếu trong database hiển thị luôn ACTIVE, BLOCKED, DEACTIVATE, sẽ chẳng cần bạn hoặc bất kỳ ai giải thích gì thêm. Việc trao đổi giữa các hệ thống cũng trở nên vô cùng đơn giản vì nó đã quá rõ ràng rồi.
Vấn đề tối ưu dữ liệu cũng không cần quá nghiêm trọng hóa. Bạn chỉ tốn thêm vài byte mỗi record để lưu kiểu chữ thay vì số. Quá nhỏ so với những lợi ích nó mang lại.
Do đó, mình khuyến nghị sử dụng Enumerated STRING. Vừa trực quan lại dễ dàng khai báo vì không yêu cầu viết thêm bất kỳ converter nào khác.

Reference