JPAでの永続的な列挙
1. 概要
JPAバージョン2.0以下では、Enum値をデータベース列にマップする便利な方法はありません。 各オプションには、制限と欠点があります。 これらの問題は、JPA2.1の機能を使用することで回避できます。
このチュートリアルでは、JPAを使用してデータベースに列挙型を永続化するために必要なさまざまな可能性を見ていきます。 また、それらの長所と短所について説明し、簡単なコード例を示します。
2. @Enumeratedアノテーションの使用
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)アノテーションをenumフィールドに配置すると、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. マッピング文字列値
同様に、enumフィールドに @Enumerated(EnumType.STRING)の注釈を付けると、JPAはエンティティを格納するときに Enum.name()値を使用します。
2番目の列挙型を作成しましょう:
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アノテーションの使用
データベース内の永続的な列挙型を処理する必要があるもう1つのオプションは、標準のJPAコールバックメソッドを使用することです。 @PostLoadイベントと@PrePersistイベントで列挙を前後にマッピングできます。
アイデアは、エンティティに2つの属性を持つことです。 1つ目はデータベース値にマップされ、2つ目は実際の列挙値を保持する@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);
}
}
また、 Priority.of()メソッドを追加して、int値に基づいてPriorityインスタンスを簡単に取得できるようにしました。
ここで、 Article クラスで使用するには、2つの属性を追加し、コールバックメソッドを実装する必要があります。
@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]
このオプションを使用すると、前述のソリューションと比較してデータベース値の表現をより柔軟に選択できますが、理想的ではありません。 エンティティ内の単一の列挙型を表す2つの属性を持つことは正しくないと感じています。
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の値をtrueに設定して、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を使用する場合のオプションと、JPA2.1以降で使用可能な新しいAPIを紹介しました。
JPAで列挙型を処理する可能性はこれらだけではないことに注意してください。 PostgreSQLなどの一部のデータベースは、列挙値を格納するための専用の列タイプを提供します。ただし、このようなソリューションはこの記事の範囲外です。
経験則として、JPA 2.1以降を使用している場合は、常にAttributeConverterインターフェースと@Converterアノテーションを使用する必要があります。
いつものように、すべてのコード例はGitHubリポジトリで入手できます。