web-dev-qa-db-fra.com

Map enum dans JPA avec des valeurs fixes?

Je cherche les différentes manières de cartographier une énumération à l'aide de JPA. Je veux surtout définir la valeur entière de chaque entrée enum et ne sauvegarder que la valeur entière.

@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;
}

Une solution simple consiste à utiliser l'annotation Enumerated avec EnumType.ORDINAL:

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

Mais dans ce cas, JPA mappe l'indice d'enum (0,1,2) et non la valeur que je veux (100 200 300).

Les deux solutions que j'ai trouvées ne semblent pas simples ...

Première solution

Une solution, proposé ici , utilise @PrePersist et @PostLoad pour convertir l'énum en un autre champ et marquer le champ enum comme étant transitoire:

@Basic
private int intValueForAnEnum;

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

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

Deuxième solution

La deuxième solution proposée ici proposait un objet de conversion générique, mais semble toujours lourde et orientée vers l'hibernation (@Type ne semble pas exister dans 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")
            }
)

Y a-t-il d'autres solutions?

J'ai plusieurs idées en tête mais je ne sais pas si elles existent dans JPA:

  • utiliser les méthodes setter et getter du membre droit de la classe d'autorité lors du chargement et de la sauvegarde de l'objet d'autorité
  • une idée équivalente serait de dire à JPA quelles sont les méthodes de Right enum pour convertir enum en int et int en enum
  • Parce que j'utilise Spring, y a-t-il un moyen de dire à JPA d'utiliser un convertisseur spécifique (RightEditor)?
178
Kartoch

Pour les versions antérieures à JPA 2.1, JPA ne fournit que deux façons de traiter les énumérations, par leur name ou par leur ordinal. Et le JPA standard ne prend pas en charge les types personnalisés. Alors:

  • Si vous souhaitez effectuer des conversions de types personnalisés, vous devez utiliser une extension fournisseur (avec Hibernate UserType, EclipseLink Converter, etc.). (la deuxième solution). ~ ou ~
  • Vous devrez utiliser les astuces @PrePersist et @PostLoad (la première solution). ~ ou ~
  • Annoter le getter et le setter en prenant et en retournant la int valeur ~ ou ~
  • Utilisez un attribut entier au niveau de l'entité et effectuez une traduction dans les getters et les setters.

Je vais illustrer la dernière option (il s’agit d’une implémentation de base, modifiez-la si nécessaire):

@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();
    }

}
163
Pascal Thivent

C'est désormais possible avec JPA 2.1:

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

Plus de détails:

63
Tvaroh

La meilleure approche serait de mapper un identifiant unique sur chaque type d’énum, ​​évitant ainsi les pièges de ORDINAL et de STRING. Voir ceci post qui décrit 5 façons de mapper un enum.

Tiré du lien ci-dessus:

1 & 2. Utilisation de @Enumerated

Vous pouvez actuellement mapper des enums au sein de vos entités JPA à l'aide de l'annotation @Enumerated. Malheureusement, EnumType.STRING et EnumType.ORDINAL ont leurs limites.

Si vous utilisez EnumType.String, si vous renommez l'un de vos types enum, votre valeur enum sera désynchronisée par rapport aux valeurs enregistrées dans la base de données. Si vous utilisez EnumType.ORDINAL, la suppression ou la réorganisation des types de votre énumération entraînera une correspondance entre les valeurs enregistrées dans la base de données et les types d’énums incorrects.

Ces deux options sont fragiles. Si l'énumération est modifiée sans effectuer de migration de base de données, vous pouvez désopodiser l'intégrité de vos données.

3. Rappel du cycle de vie

Une solution possible consisterait à utiliser les annotations de rappel de cycle de vie JPA, @PrePersist et @PostLoad. Cela semble assez moche car vous allez maintenant avoir deux variables dans votre entité. Un mappage de la valeur stockée dans la base de données et l'autre, l'énumération réelle.

4. Mappage d'un identifiant unique sur chaque type enum

La solution recommandée consiste à mapper votre enum sur une valeur fixe, ou ID, définie dans l’enum. Le mappage sur une valeur fixe prédéfinie rend votre code plus robuste. Toute modification de l'ordre des types enums, ou la refactorisation des noms, n'aura aucun effet négatif.

5. Utilisation de Java EE7 @Convert

Si vous utilisez JPA 2.1, vous avez la possibilité d’utiliser la nouvelle annotation @Convert. Cela nécessite la création d'une classe de convertisseur, annotée avec @Converter, à l'intérieur de laquelle vous définiriez les valeurs à enregistrer dans la base de données pour chaque type enum. Dans votre entité, vous annoterez ensuite votre enum avec @Convert.

Mes préférences: (numéro 4)

La raison pour laquelle je préfère définir mes identifiants dans l'énumération, par opposition à l'utilisation d'un convertisseur, est une bonne encapsulation. Seul le type enum doit connaître son ID et seule l'entité doit savoir comment elle mappe l'enum sur la base de données.

Voir l'original post pour l'exemple de code.

16
Chris Ritchie

À partir de JPA 2.1, vous pouvez utiliser AttributeConverter .

Créez une classe énumérée comme ceci:

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;
    }
}

Et créez un convertisseur comme celui-ci:

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);
    }
}

Sur l'entité dont vous avez juste besoin:

@Column(name = "node_type_code")

Votre chance avec @Converter(autoApply = true) peut varier d'un conteneur à l'autre, mais il a été testé pour fonctionner avec Wildfly 8.1.0. Si cela ne fonctionne pas, vous pouvez ajouter @Convert(converter = NodeTypeConverter.class) sur la colonne de la classe d'entité.

15
Pool

Le problème est, je pense, que JPA n’a jamais été créée avec l’idée que nous pourrions déjà avoir un schéma complexe préexistant.

Je pense qu'il en résulte deux inconvénients principaux, spécifiques à Enum:

  1. La limitation d'utiliser name () et ordinal (). Pourquoi ne pas simplement marquer un getter avec @Id, comme nous le faisons avec @Entity?
  2. Les énumérations sont généralement représentées dans la base de données pour permettre l’association à toutes sortes de métadonnées, y compris un nom propre, un nom descriptif, peut-être quelque chose avec la localisation, etc. Nous avons besoin de la facilité d’utilisation d’une énumération combinée à la souplesse d’une entité.

Aidez ma cause et votez sur JPA_SPEC-47

Cela ne serait-il pas plus élégant que d'utiliser un @Converter pour résoudre le problème?

// 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;
  }
}
10
YoYo

Peut-être étroitement lié code de 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();
    }

}
3
Rafiq

Je ferais ce qui suit:

Déclarez séparément l'énumération, dans son propre fichier:

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; }


}

Déclarer une nouvelle entité JPA nommée Right

@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);
    }


}

Vous aurez également besoin d’un convertisseur pour recevoir ces valeurs (JPA 2.1 uniquement et il ya un problème que je ne discuterai pas ici avec ces énumérations qui doivent être persistées directement à l’aide du convertisseur, ce sera donc une route à sens unique uniquement)

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);
    }

}

L'entité de l'autorité:

@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;

}

De cette façon, vous ne pouvez pas définir directement le champ enum. Cependant, vous pouvez définir le champ Droit dans Autorité à l'aide de

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

Et si vous avez besoin de comparer, vous pouvez utiliser:

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

Ce qui est plutôt cool, je pense. Ce n'est pas tout à fait correct, car le convertisseur n'est pas conçu pour être utilisé avec enum. En fait, la documentation indique de ne jamais l'utiliser à cette fin. Vous devez plutôt utiliser l'annotation @Enumerated. Le problème est qu’il n’ya que deux types d’énumérations: ORDINAL ou STRING, mais ORDINAL est délicat et peu sûr.


Cependant, si cela ne vous satisfait pas, vous pouvez faire quelque chose d'un peu plus simple et plus simple (ou non).

Voyons voir.

Le 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; }


}

et l'entité de l'autorité

@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;

}

Dans cette seconde idée, ce n’est pas une situation parfaite puisque nous piratons l’attribut ordinal, mais c’est un codage beaucoup plus petit.

Je pense que la spécification JPA devrait inclure EnumType.ID où le champ de valeur enum devrait être annoté avec une sorte d'annotation @EnumId.

1
Carlos Cariello