1. 소개
JPA 버전 2.0 이하에서는 Enum 값을 데이터베이스 열에 매핑하는 편리한 방법이 없습니다 . 각 옵션에는 한계와 단점이 있습니다. 이러한 문제는 JPA 2.1을 사용하여 피할 수 있습니다. 특징.
이 사용방법(예제)에서는 JPA를 사용하여 데이터베이스에서 열거형을 유지해야 하는 다양한 가능성을 살펴보겠습니다. 또한 장점과 단점을 설명하고 간단한 코드 예제를 제공합니다.
2. @Enumerated 어노테이션 사용 하기
enum 값을 2.1 이전 JPA의 데이터베이스 표현과 매핑하는 가장 일반적인 옵션입니다. @Enumerated 어노테이션 을 사용하는 것 입니다. 이런 식으로 JPA 공급자에게 열거형을 서수 또는 문자열 값 으로 변환하도록 지시할 수 있습니다 .
이 섹션에서는 두 가지 옵션을 모두 살펴보겠습니다.
하지만 먼저 이 예제 전체에서 사용할 간단한 @Entity 를 만들어 보겠습니다 .
@Entity
public class Article {
@Id
private int id;
private String title;
// standard constructors, getters and setters
}
2.1. 서수 값 매핑
우리가 넣으면 @Enumerated (EnumType.ORDINAL) 열거 필드에 어노테이션을, JPA는 사용 Enum.ordinal () 데이터베이스에 지정된 엔티티를 지속 할 때 값입니다.
첫 번째 열거형을 소개하겠습니다.
public enum Status {
OPEN, REVIEW, APPROVED, REJECTED;
}
다음으로 Article 클래스에 추가하고 @Enumerated(EnumType.ORDINAL) 어노테이션을 추가해 보겠습니다 .
@Entity
public class Article {
@Id
private int id;
private String title;
@Enumerated(EnumType.ORDINAL)
private Status status;
}
이제 Article 엔터티를 유지할 때 :
Article article = new Article();
article.setId(1);
article.setTitle("ordinal title");
article.setStatus(Status.OPEN);
JPA는 다음 SQL 문을 트리거합니다.
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]
이러한 종류의 매핑 문제는 열거형을 수정해야 할 때 발생합니다. 중간에 새 값을 추가하거나 열거형의 순서를 재정렬하면 기존 데이터 모델이 중단 됩니다.
이러한 문제는 파악하기 어려울 뿐만 아니라 모든 데이터베이스 레코드를 업데이트해야 하므로 해결하기 어려울 수 있습니다.
2.2. 문자열 값 매핑
유사하게, JPA는 enum 필드에 @Enumerated(EnumType.STRING) 어노테이션을 추가하면 엔티티를 저장할 때 Enum.name() 값을 사용합니다 .
두 번째 열거형을 만들어 보겠습니다.
public enum Type {
INTERNAL, EXTERNAL;
}
이를 Article 클래스에 추가하고 @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;
}
이제 Article 엔터티를 유지할 때 :
Article article = new Article();
article.setId(2);
article.setTitle("string title");
article.setType(Type.EXTERNAL);
JPA는 다음 SQL 문을 실행합니다.
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]
@Enumerated(EnumType.STRING) 을 사용하면 새 열거형 값을 안전하게 추가하거나 열거형 순서를 변경할 수 있습니다. 그러나 열거형 값의 이름을 바꾸면 여전히 데이터베이스 데이터가 손상됩니다.
또한 이 데이터 표현은 @Enumerated(EnumType.ORDINAL) 옵션에 비해 훨씬 읽기 쉽지만 필요한 것보다 훨씬 더 많은 공간을 소비합니다. 이것은 많은 양의 데이터를 처리해야 할 때 중요한 문제로 판명될 수 있습니다.
사용 3. @PostLoad 및 @PrePersist 어노테이션
데이터베이스에서 지속되는 열거형을 처리해야 하는 또 다른 옵션은 표준 JPA 콜백 메서드를 사용하는 것입니다. 우리는 앞뒤로 우리의 열거를 매핑 할 수 있습니다 @PostLoad 및 @PrePersist 이벤트.
아이디어는 엔티티에 두 개의 속성을 갖는 것입니다. 첫 번째는 데이터베이스 값에 매핑되고 두 번째 는 실제 열거형 값을 보유하는 @Transient 필드입니다. 그러면 임시 속성이 비즈니스 논리 코드에서 사용됩니다.
개념을 더 잘 이해하기 위해 새 열거형을 만들고 매핑 논리에서 int 값을 사용하겠습니다 .
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);
}
}
또한 int 값을 기반으로 Priority 인스턴스를 쉽게 가져올 수 있도록 Priority.of() 메서드를 추가했습니다 .
이제 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;
@PostLoad
void fillTransient() {
if (priorityValue > 0) {
this.priority = Priority.of(priorityValue);
}
}
@PrePersist
void fillPersistent() {
if (priority != null) {
this.priorityValue = priority.getPriority();
}
}
}
이제 Article 엔터티를 유지할 때 :
Article article = new Article();
article.setId(3);
article.setTitle("callback title");
article.setPriority(Priority.HIGH);
JPA는 다음 SQL 쿼리를 트리거합니다.
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]
이 옵션을 사용하면 이전에 설명한 솔루션에 비해 데이터베이스 값의 표현을 선택할 때 더 많은 유연성을 얻을 수 있지만 이상적이지는 않습니다. 엔터티에서 단일 열거형을 나타내는 두 개의 속성을 갖는 것은 옳지 않다고 생각합니다. 또한 이러한 유형의 매핑을 사용하면 JPQL 쿼리에서 열거형 값을 사용할 수 없습니다.
4. JPA 2.1 @Converter 어노테이션 사용
위에 표시된 솔루션의 한계를 극복하기 위해 JPA 2.1 릴리스는 엔티티 속성을 데이터베이스 값으로 또는 그 반대로 변환하는 데 사용할 수 있는 새로운 표준화된 API를 도입했습니다. 우리가 해야 할 일은 javax.persistence.AttributeConverter 를 구현하는 새 클래스를 만들고 @Converter로 어노테이션을 추가하는 것입니다 .
실용적인 예를 살펴보겠습니다. 하지만 먼저 평소와 같이 새 열거형을 만듭니다.
public enum Category {
SPORT("S"), MUSIC("M"), TECHNOLOGY("T");
private String code;
private Category(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
또한 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;
}
이제 새 CategoryConverter를 만들어 보겠습니다 .
@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);
}
}
우리는 설정 한 @Converter 의의 값 autoApply을 에 진정한 JPA는 자동으로 모든 매핑 된 속성에 전환 논리를 적용 할 정도로 분류 유형입니다. 그렇지 않으면 엔티티 필드에 @Converter 어노테이션을 직접 넣어야 합니다 .
이제 Article 엔터티를 유지해 보겠습니다 .
Article article = new Article();
article.setId(4);
article.setTitle("converted title");
article.setCategory(Category.MUSIC);
그런 다음 JPA는 다음 SQL 문을 실행합니다.
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]
보시다시피 AttributeConverter 인터페이스를 사용하면 열거형을 해당 데이터베이스 값으로 변환하는 자체 규칙을 간단히 설정할 수 있습니다. 또한 이미 유지된 데이터를 손상시키지 않고 안전하게 새 열거형 값을 추가하거나 기존 열거형 값을 변경할 수 있습니다.
전체 솔루션은 구현이 간단하며 이전 섹션에 제공된 옵션의 모든 단점을 해결합니다.
5. JPQL에서 열거형 사용하기
이제 JPQL 쿼리에서 열거형을 사용하는 것이 얼마나 쉬운지 봅시다.
Category.SPORT 범주가 있는 모든 Article 엔터티 를 찾으려면 다음 문을 실행해야 합니다.
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();
이 경우 정규화된 열거형 이름을 사용해야 한다는 점에 유의하는 것이 중요합니다.
물론 정적 쿼리에 국한되지 않습니다. 명명된 매개변수를 사용하는 것은 완전히 합법적입니다.
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();
위의 예는 동적 쿼리를 형성하는 매우 편리한 방법을 제공합니다.
또한 정규화된 이름을 사용할 필요가 없습니다.
6. 결론
이 사용방법(예제)에서는 데이터베이스에서 열거형 값을 유지하는 다양한 방법을 다루었습니다. 버전 2.0 이하에서 JPA를 사용할 때의 옵션과 JPA 2.1 이상에서 사용할 수 있는 새로운 API를 제시했습니다.
JPA에서 열거형을 처리할 수 있는 유일한 가능성은 이것이 아닙니다. PostgreSQL과 같은 일부 데이터베이스는 열거형 값을 저장하기 위한 전용 열 유형을 제공합니다. 그러나 이러한 솔루션은 이 문서의 범위를 벗어납니다.
경험상 JPA 2.1 이상을 사용하는 경우 항상 AttributeConverter 인터페이스와 @Converter 어노테이션을 사용해야 합니다 .
평소와 같이 모든 코드 예제는 GitHub 리포지토리 에서 사용할 수 있습니다 .