JPAの列挙型を固定値でマップしますか?


192

JPAを使用して列挙型をマップするさまざまな方法を探しています。特に、各enumエントリの整数値を設定し、整数値のみを保存したいと考えています。

@Entity
@Table(name = "AUTHORITY_")
public class Authority implements Serializable {

  public enum Right {
      READ(100), WRITE(200), EDITOR (300);

      private int value;

      Right(int value) { this.value = value; }

      public int getValue() { return value; }
  };

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "AUTHORITY_ID")
  private Long id;

  // the enum to map : 
  private Right right;
}

簡単な解決策は、EnumType.ORDINALでEnumeratedアノテーションを使用することです。

@Column(name = "RIGHT")
@Enumerated(EnumType.ORDINAL)
private Right right;

しかし、この場合、JPAは列挙インデックス(0,1,2)をマップし、必要な値(100,200,300)はマップしません。

私が見つけた2つの解決策は単純ではないようです...

最初の解決策

ここ提案されているソリューションは、@ PrePersistと@PostLoadを使用して列挙型を他のフィールドに変換し、列挙型フィールドを一時的としてマークします。

@Basic
private int intValueForAnEnum;

@PrePersist
void populateDBFields() {
  intValueForAnEnum = right.getValue();
}

@PostLoad
void populateTransientFields() {
  right = Right.valueOf(intValueForAnEnum);
}

2番目のソリューション

ここで提案された2番目のソリューション、一般的な変換オブジェクトを提案しましたが、それでも重くて休止状態指向です(@TypeはJava EEには存在しないようです)。

@Type(
    type = "org.appfuse.tutorial.commons.hibernate.GenericEnumUserType",
    parameters = {
            @Parameter(
                name  = "enumClass",                      
                value = "Authority$Right"),
            @Parameter(
                name  = "identifierMethod",
                value = "toInt"),
            @Parameter(
                name  = "valueOfMethod",
                value = "fromInt")
            }
)

他の解決策はありますか?

私はいくつかのアイデアを念頭に置いていますが、それらがJPAに存在するかどうかはわかりません。

  • Authorityオブジェクトを読み込んで保存するときに、Authority Classの正しいメンバーのセッターメソッドとゲッターメソッドを使用する
  • 同等のアイデアは、JPAにenumをintに変換し、intをenumに変換するRight enumのメソッドは何かを伝えることです。
  • Springを使用しているため、JPAに特定のコンバーター(RightEditor)を使用するように指示する方法はありますか?

7
ORDINALを使用するのは奇妙です。誰かが列挙のアイテムの場所を変更し、データベースが災害になります
ナターシャKP

2
Nameを使用する場合も同じではありません-誰かが列挙型の名前を変更する可能性があり、それらは再びデータベースと同期しなくなります...
topchef

2
@NatashaKPに同意します。序数は使用しないでください。名前を変更するために、そのようなことはありません。実際には古い列挙型を削除し、新しい名前で新しい列挙型を追加しているため、保存されているデータは同期していません(セマンティクス、おそらく:P)。
Svend Hansen

はい、私が知っている5つの解決策があります。詳細な回答がある場合は、以下の私の回答を参照してください。
Chris Ritchie

回答:


168

JPA 2.1より前のバージョンの場合、JPAは列挙型を処理する方法として、nameまたはによる方法を2つしか提供していませんordinal。また、標準のJPAはカスタムタイプをサポートしていません。そう:

  • カスタム型変換を行う場合は、プロバイダー拡張機能(Hibernate UserType、EclipseLink Converterなど)を使用する必要があります。(2番目のソリューション)。〜または〜
  • @PrePersistおよび@PostLoadトリックを使用する必要があります(最初の解決策)。〜または〜
  • int値を取得して返すゲッターとセッターに注釈を付ける〜or〜
  • エンティティレベルで整数属性を使用し、ゲッターとセッターで変換を実行します。

最新のオプションを説明します(これは基本的な実装であり、必要に応じて調整します)。

@Entity
@Table(name = "AUTHORITY_")
public class Authority implements Serializable {

    public enum Right {
        READ(100), WRITE(200), EDITOR (300);

        private int value;

        Right(int value) { this.value = value; }    

        public int getValue() { return value; }

        public static Right parse(int id) {
            Right right = null; // Default
            for (Right item : Right.values()) {
                if (item.getValue()==id) {
                    right = item;
                    break;
                }
            }
            return right;
        }

    };

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "AUTHORITY_ID")
    private Long id;

    @Column(name = "RIGHT_ID")
    private int rightId;

    public Right getRight () {
        return Right.parse(this.rightId);
    }

    public void setRight(Right right) {
        this.rightId = right.getValue();
    }

}

50
悲しいことに、JPAはこれをネイティブにサポートしていません
Jaime Hablutzel、2011

20
@jaime同意した!開発者が、そのint値や名前ではなく、フィールド/プロパティの1つの値として列挙型を永続化したいと考えるのはおかしいですか?どちらも非常に「壊れやすく」、リファクタリングに適していません。また、名前の使用は、Javaとデータベースの両方で同じ命名規則を使用していることも前提としています。性別を例にとります。これはデータベースでは単に「M」または「F」として定義されている可能性がありますが、JavaでGender.MまたはGender.Fの代わりにGender.MALEおよびGender.FEMALEを使用することを妨げるものではありません。
spaaarky21

2
名前と序数の両方が一意であることが保証されているのに対し、他の値またはフィールドは一意でないことが理由である可能性があります。順序が変わる可能性がある(序数を使用しない)と名前も変更できる(列挙型名を変更しないでください:P)が、他の値も変わる可能性があることは事実です...わかりません別の値を保存する機能を追加することで大きな価値が生まれます。
Svend Hansen

2
実際には、値が表示されます...それでも編集できる場合は、上記のコメントのその部分を削除します:P:D
Svend Hansen

13
JPA2.1はコンバーターをネイティブでサポートします。somethoughtsonjava.blogspot.fi/2013/10/…を
drodil

69

これは現在、JPA 2.1で可能です。

@Column(name = "RIGHT")
@Enumerated(EnumType.STRING)
private Right right;

詳細:


2
今何ができるの?確かに私達は使用することができ@Converterますが、enum箱から出して、よりエレガントに処理する必要があります!
YoYo 2015

4
「Answer」は、AttributeConverterBUT の使用について言及する2つのリンクを参照しており、そのようなことは何もせず、OPにも応答しないコードを引用しています。

@ DN1は自由に改善してください
Tvaroh、

1
それはあなたの「答え」なので、「改善する」べきです。すでに回答がありますAttributeConverter

1
追加後は完全に機能します:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28 '21 / 03/18

22

JPA 2.1以降では、AttributeConverterを使用できます。

次のように列挙型クラスを作成します。

public enum NodeType {

    ROOT("root-node"),
    BRANCH("branch-node"),
    LEAF("leaf-node");

    private final String code;

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

    public String getCode() {
        return code;
    }
}

そして、このようなコンバーターを作成します:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class NodeTypeConverter implements AttributeConverter<NodeType, String> {

    @Override
    public String convertToDatabaseColumn(NodeType nodeType) {
        return nodeType.getCode();
    }

    @Override
    public NodeType convertToEntityAttribute(String dbData) {
        for (NodeType nodeType : NodeType.values()) {
            if (nodeType.getCode().equals(dbData)) {
                return nodeType;
            }
        }

        throw new IllegalArgumentException("Unknown database value:" + dbData);
    }
}

必要なエンティティについて:

@Column(name = "node_type_code")

運の@Converter(autoApply = true)良さはコンテナによって異なりますが、Wildfly 8.1.0で動作することがテストされています。機能しない場合は@Convert(converter = NodeTypeConverter.class)、エンティティクラスの列を追加できます。


「values()」は「NodeType.values()」である必要があります
Curtis Yallop

17

最善の方法は、一意のIDを各列挙型にマップして、ORDINALとSTRINGの落とし穴を回避することです。この投稿を見る列挙型をマッピングする5つの方法を概説するを。

上記のリンクから取得:

1&2。@Enumeratedの使用

現在、@ Enumeratedアノテーションを使用してJPAエンティティ内で列挙型をマップする方法は2つあります。残念ながら、EnumType.STRINGとEnumType.ORDINALの両方に制限があります。

EnumType.Stringを使用する場合、列挙型のいずれかの名前を変更すると、列挙値がデータベースに保存されている値と同期しなくなります。EnumType.ORDINALを使用する場合、列挙内の型を削除または並べ替えると、データベースに保存された値が誤った列挙型にマッピングされます。

これらのオプションはどちらも脆弱です。データベースの移行を実行せずに列挙型を変更すると、データの整合性が危険にさらされる可能性があります。

3.ライフサイクルコールバック

可能な解決策は、JPAライフサイクルコールバックアノテーション、@ PrePersistおよび@PostLoadを使用することです。エンティティに2つの変数が含まれるようになるため、これは非常に醜く感じられます。1つはデータベースに保存された値をマッピングし、もう1つは実際の列挙型をマッピングします。

4.一意のIDを各列挙型にマッピングする

推奨される解決策は、列挙型を、列挙型内で定義された固定値またはIDにマップすることです。定義済みの固定値にマッピングすると、コードがより堅牢になります。列挙型の順序や名前のリファクタリングを変更しても、悪影響はありません。

5. Java EE7 @Convertの使用

JPA 2.1を使用している場合は、新しい@Convertアノテーションを使用するオプションがあります。これには、@ Converterで注釈が付けられたコンバータークラスを作成する必要があります。この内部で、列挙型ごとにデータベースに保存する値を定義します。エンティティ内では、列挙型に@Convertアノテーションを付けます。

私の好み:(番号4)

コンバーターを使用するのではなく、列挙内でIDを定義することを好む理由は、カプセル化が適切であるためです。enumタイプのみがそのIDを認識し、エンティティのみがenumをデータベースにマップする方法を認識している必要があります。

コード例については、元の投稿を参照してください。


1
javax.persistence.Converterはシンプルで、@ Converter(autoApply = true)を使用すると、ドメインクラスに@Convertアノテーションを付けないようにすることができます
Rostislav Matl

9

問題は、JPAが複雑な既存のスキーマをすでに配置している可能性があるという考えを念頭に置いて決して受け入れられなかったことだと思います。

これにはEnumに固有の2つの主な欠点があると思います。

  1. name()およびordinal()の使用の制限。@Entityで行うように、@ Idでゲッターをマークするだけではどうですか?
  2. Enumは通常、適切な名前、説明的な名前、ローカライズされたものなど、あらゆる種類のメタデータとの関連付けを可能にするデータベース内の表現を持っています。Enumの使いやすさとエンティティの柔軟性を組み合わせる必要があります。

私の大義を助け、JPA_SPEC-47に投票してください

これは、@ Converterを使用して問題を解決するよりもエレガントではないでしょうか?

// Note: this code won't work!!
// it is just a sample of how I *would* want it to work!
@Enumerated
public enum Language {
  ENGLISH_US("en-US"),
  ENGLISH_BRITISH("en-BR"),
  FRENCH("fr"),
  FRENCH_CANADIAN("fr-CA");
  @ID
  private String code;
  @Column(name="DESCRIPTION")
  private String description;

  Language(String code) {
    this.code = code;
  }

  public String getCode() {
    return code;
  }

  public String getDescription() {
    return description;
  }
}

4

Pascalの関連コードを閉じる可能性があります

@Entity
@Table(name = "AUTHORITY_")
public class Authority implements Serializable {

    public enum Right {
        READ(100), WRITE(200), EDITOR(300);

        private Integer value;

        private Right(Integer value) {
            this.value = value;
        }

        // Reverse lookup Right for getting a Key from it's values
        private static final Map<Integer, Right> lookup = new HashMap<Integer, Right>();
        static {
            for (Right item : Right.values())
                lookup.put(item.getValue(), item);
        }

        public Integer getValue() {
            return value;
        }

        public static Right getKey(Integer value) {
            return lookup.get(value);
        }

    };

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "AUTHORITY_ID")
    private Long id;

    @Column(name = "RIGHT_ID")
    private Integer rightId;

    public Right getRight() {
        return Right.getKey(this.rightId);
    }

    public void setRight(Right right) {
        this.rightId = right.getValue();
    }

}

2

私は以下を行います:

独自のファイルで列挙型を個別に宣言します。

public enum RightEnum {
      READ(100), WRITE(200), EDITOR (300);

      private int value;

      private RightEnum (int value) { this.value = value; }


      @Override
      public static Etapa valueOf(Integer value){
           for( RightEnum r : RightEnum .values() ){
              if ( r.getValue().equals(value))
                 return r;
           }
           return null;//or throw exception
     }

      public int getValue() { return value; }


}

Rightという新しいJPAエンティティを宣言します。

@Entity
public class Right{
    @Id
    private Integer id;
    //FIElDS

    // constructor
    public Right(RightEnum rightEnum){
          this.id = rightEnum.getValue();
    }

    public Right getInstance(RightEnum rightEnum){
          return new Right(rightEnum);
    }


}

また、この値を受け取るにはコンバーターも必要です(JPA 2.1のみです。コンバーターを使用して直接永続化するために、これらの列挙型についてここでは説明しませんので、片道のみとなります)。

import mypackage.RightEnum;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

/**
 * 
 * 
 */
@Converter(autoApply = true)
public class RightEnumConverter implements AttributeConverter<RightEnum, Integer>{

    @Override //this method shoudn´t be used, but I implemented anyway, just in case
    public Integer convertToDatabaseColumn(RightEnum attribute) {
        return attribute.getValue();
    }

    @Override
    public RightEnum convertToEntityAttribute(Integer dbData) {
        return RightEnum.valueOf(dbData);
    }

}

Authorityエンティティ:

@Entity
@Table(name = "AUTHORITY_")
public class Authority implements Serializable {


  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "AUTHORITY_ID")
  private Long id;

  // the **Entity** to map : 
  private Right right;

  // the **Enum** to map (not to be persisted or updated) : 
  @Column(name="COLUMN1", insertable = false, updatable = false)
  @Convert(converter = RightEnumConverter.class)
  private RightEnum rightEnum;

}

この方法では、enumフィールドに直接設定することはできません。ただし、AuthorityのRightフィールドを使用して設定できます。

autorithy.setRight( Right.getInstance( RightEnum.READ ) );//for example

比較する必要がある場合は、以下を使用できます。

authority.getRight().equals( RightEnum.READ ); //for example

かなりクールだと思います。コンバーターは列挙型での使用を意図していないため、完全に正しくはありません。実際には、ドキュメントにはこの目的には使用しないと記載されています。代わりに@Enumeratedアノテーションを使用する必要があります。問題は、列挙型がORDINALまたはSTRINGの2つしかないことですが、ORDINALは扱いが難しく、安全ではありません。


ただし、それでも満足できない場合は、もう少しハックでシンプルな(またはそうでない)ことができます。

どれどれ。

RightEnum:

public enum RightEnum {
      READ(100), WRITE(200), EDITOR (300);

      private int value;

      private RightEnum (int value) { 
            try {
                  this.value= value;
                  final Field field = this.getClass().getSuperclass().getDeclaredField("ordinal");
                  field.setAccessible(true);
                  field.set(this, value);
             } catch (Exception e) {//or use more multicatch if you use JDK 1.7+
                  throw new RuntimeException(e);
            }
      }


      @Override
      public static Etapa valueOf(Integer value){
           for( RightEnum r : RightEnum .values() ){
              if ( r.getValue().equals(value))
                 return r;
           }
           return null;//or throw exception
     }

      public int getValue() { return value; }


}

とAuthorityエンティティ

@Entity
@Table(name = "AUTHORITY_")
public class Authority implements Serializable {


  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "AUTHORITY_ID")
  private Long id;


  // the **Enum** to map (to be persisted or updated) : 
  @Column(name="COLUMN1")
  @Enumerated(EnumType.ORDINAL)
  private RightEnum rightEnum;

}

この2番目のアイデアでは、序数属性をハッキングするため、これは完全な状況ではありませんが、コーディングははるかに小さくなります。

JPA仕様にはEnumType.IDが含まれている必要があると思います。列挙値フィールドには、何らかの@EnumIdアノテーションを付ける必要があります。


2

この種のEnum JPAマッピングを解決するための私自身のソリューションは次のとおりです。

手順1 -db列にマップするすべての列挙型に使用する次のインターフェイスを記述します。

public interface IDbValue<T extends java.io.Serializable> {

    T getDbVal();

}

手順2-カスタムの汎用JPAコンバーターを次のように実装します。

import javax.persistence.AttributeConverter;

public abstract class EnumDbValueConverter<T extends java.io.Serializable, E extends Enum<E> & IDbValue<T>>
        implements AttributeConverter<E, T> {

    private final Class<E> clazz;

    public EnumDbValueConverter(Class<E> clazz){
        this.clazz = clazz;
    }

    @Override
    public T convertToDatabaseColumn(E attribute) {
        if (attribute == null) {
            return null;
        }
        return attribute.getDbVal();
    }

    @Override
    public E convertToEntityAttribute(T dbData) {
        if (dbData == null) {
            return null;
        }
        for (E e : clazz.getEnumConstants()) {
            if (dbData.equals(e.getDbVal())) {
                return e;
            }
        }
        // handle error as you prefer, for example, using slf4j:
        // log.error("Unable to convert {} to enum {}.", dbData, clazz.getCanonicalName());
        return null;
    }

}

このクラスは、on enum を使用してenum値EをタイプT(例:) のデータベースフィールドに変換します。逆も同様です。StringgetDbVal()E

手順3-手順1で定義したインターフェイスを元の列挙型に実装させます。

public enum Right implements IDbValue<Integer> {
    READ(100), WRITE(200), EDITOR (300);

    private final Integer dbVal;

    private Right(Integer dbVal) {
        this.dbVal = dbVal;
    }

    @Override
    public Integer getDbVal() {
        return dbVal;
    }
}

ステップ4-ステップ2のコンバーターRightをステップ3の列挙型に拡張します。

public class RightConverter extends EnumDbValueConverter<Integer, Right> {
    public RightConverter() {
        super(Right.class);
    }
}

手順5-最後の手順は、次のようにエンティティのフィールドに注釈を付けることです。

@Column(name = "RIGHT")
@Convert(converter = RightConverter.class)
private Right right;

結論

私見これは、多くの列挙型をマップする必要があり、列挙型自体の特定のフィールドをマッピング値として使用する場合、最もクリーンで最もエレガントなソリューションです。

同様のマッピングロジックを必要とするプロジェクトの他のすべての列挙型については、手順3〜5を繰り返すだけで済みます。

  • IDbValueenumにインターフェースを実装します。
  • 延長する EnumDbValueConverter3行のコードでをます(エンティティ内でこれを行うと、分離されたクラスの作成を回避できます)。
  • enum属性に@Convertfrom javax.persistenceパッケージで注釈を付けます。

お役に立てれば。


1
public enum Gender{ 
    MALE, FEMALE 
}



@Entity
@Table( name="clienti" )
public class Cliente implements Serializable {
...

// **1 case** - If database column type is number (integer) 
// (some time for better search performance)  -> we should use 
// EnumType.ORDINAL as @O.Badr noticed. e.g. inserted number will
// index of constant starting from 0... in our example for MALE - 0, FEMALE - 1.
// **Possible issue (advice)**: you have to add the new values at the end of
// your enum, in order to keep the ordinal correct for future values.

@Enumerated(EnumType.ORDINAL)
    private Gender gender;


// **2 case** - If database column type is character (varchar) 
// and you want to save it as String constant then ->

@Enumerated(EnumType.STRING)
    private Gender gender;

...
}

// in all case on code level you will interact with defined 
// type of Enum constant but in Database level

最初のケース(EnumType.ORDINAL

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood      0   
  2  Geoff Dalgas     0   
  3 Jarrod Jesica     1   
  4  Joel Lucy        1   
╚════╩══════════════╩════════╝

2番目のケース(EnumType.STRING

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.