JPAでの列挙の永続化

1. 前書き

JPAバージョン2.0以前では、https://www.baeldung.com/a-guide-to-java-enums [Enum]値をデータベース列にマッピングする便利な方法はありません。 各オプションには制限と欠点があります。 これらの問題は、JPA 2.1を使用することで回避できます。 特徴。
このチュートリアルでは、JPAを使用して列挙型をデータベースに永続化するさまざまな可能性について見ていきます。 また、単純なコード例を提供するだけでなく、それらの長所と短所についても説明します。

2. _ @ Enumerated_アノテーションの使用

  • 2.1より前のJPAのデータベース表現との間で列挙値をマッピングする最も一般的なオプション。 _ @ Enumerated_アノテーションを使用することです。*この方法では、JPAプロバイダーに、enumをその序数値または_String_値に変換するように指示できます。

    このセクションでは、両方のオプションを検討します。
    しかし、最初に、このチュートリアル全体で使用する単純な_ @ Entity_を作成しましょう。
@Entity
public class Article {
    @Id
    private int id;

    private String title;

    // standard constructors, getters and setters
}

2.1. 序数値のマッピング

  • enumフィールドに_ @ Enumerated(EnumType.ORDINAL)_注釈を付けると、JPAは_https://docs.oracle.com/javase/8/docs/api/java/lang/Enum.html#を使用しますordinal-[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]
この種のマッピングには、enumを変更する必要があるときに問題が発生します。 *新しい値を中央に追加するか、列挙型の順序を変更すると、既存のデータモデルが壊れます*。
このような問題は、すべてのデータベースレコードを更新する必要があるため、修正するのが難しいだけでなく、キャッチするのが難しい場合があります。

2.2. 文字列値のマッピング

*同様に、JPAはエンティティを格納するときに_https://docs.oracle.com/javase/8/docs/api/java/lang/Enum.html#name-- [Enum.name()] _値を使用します。列挙型フィールドに_ @ Enumerated(EnumType.STRING)_。*アノテーションを付けます
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つの属性を持つのは適切ではありません。 *さらに、このタイプのマッピングを使用する場合、JPQLクエリで列挙値を使用することはできません。 *

4. JPA 2.1 _ @ Converter_アノテーションの使用

*上記のソリューションの制限を克服するために、JPA 2.1リリースでは、エンティティ属性をデータベース値に、またはその逆に変換するために使用できる新しい標準化されたAPIを導入しました。*実装する新しいクラスを作成するだけです。 _javax.persistence.AttributeConverter_に[email protected]_アノテーションを付けます
実用的な例を見てみましょう。 しかし、最初に、いつものように、新しい列挙型を作成します。
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);
    }
}
_autoApply_の_ @ Converter_’sの値を_true_に設定して、JPAが_Category_タイプのマッピングされたすべての属性に変換ロジックを自動的に適用するようにしました。 それ以外の場合は、エンティティのフィールドに_ @ 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_インターフェイスを使用すると、enumを対応するデータベース値に変換する独自のルールを簡単に設定できます。 *さらに、既存のデータを壊すことなく、新しい列挙値を安全に追加したり、既存の値を変更したりできます。*
全体的なソリューションは実装が簡単で、前のセクションで示したオプションのすべての欠点に対処します。

5. 結論

このチュートリアルでは、列挙値をデータベースに永続化するさまざまな方法について説明しました。 バージョン2.0以下でJPAを使用する場合のオプションと、JPA 2.1以降で使用可能な新しいAPIを紹介しました。
JPAで列挙型を処理できるのはこれらだけではないことに注意してください。 https://www.postgresql.org/docs/current/datatype-enum.html[PostgreSQLのような一部のデータベースは、enum値を格納する専用の列タイプを提供します。]ただし、このようなソリューションはこの記事の範囲外です。
*経験則として、JPA 2.1以降を使用している場合は、常に_AttributeConverter_インターフェイスと_ @ Converter_アノテーションを使用する必要があります。*
いつものように、すべてのコード例はhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa[GitHubリポジトリ]から入手できます。