214 votes

Map enum dans JPA avec des valeurs fixes ?

Je cherche les différentes façons de mapper un enum en utilisant JPA. Je veux surtout définir la valeur entière de chaque entrée de l'énumération 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'index de l'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'enum en un autre champ et marquer le champ enum comme 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é ici propose un objet de conversion générique, mais semble encore lourd et orienté hibernation (@Type ne semble pas exister en 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")
            }
)

Existe-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 Authority lors du chargement et de la sauvegarde de l'objet Authority
  • une idée équivalente serait d'indiquer à JPA quelles sont les méthodes de Right enum pour convertir enum en int et int en enum
  • Comme j'utilise Spring, y a-t-il un moyen de dire à JPA d'utiliser un convertisseur spécifique (RightEditor) ?

9 votes

Il est étrange d'utiliser ORDINAL car quelqu'un va parfois changer la place des éléments dans l'énumération et la base de données deviendra un désastre.

2 votes

Ne serait-ce pas la même chose pour l'utilisation de Name - quelqu'un peut changer le(s) nom(s) de l'enum et ils sont à nouveau désynchronisés avec la base de données...

2 votes

Je suis d'accord avec @NatashaKP. N'utilisez pas l'ordinal. Pour le changement de nom, il n'y a pas de telle chose. Vous supprimez en fait l'ancienne énumération et en ajoutez une nouvelle avec un nouveau nom, donc oui, toute donnée stockée serait désynchronisée (sémantique, peut-être :P ).

179voto

Pascal Thivent Points 295221

Pour les versions antérieures à JPA 2.1, JPA ne fournit que deux manières de traiter les enums, par leur name ou par leur ordinal . Et le JPA standard ne supporte pas les types personnalisés. Donc :

  • Si vous souhaitez effectuer des conversions de type personnalisées, vous devrez utiliser une extension de fournisseur (avec Hibernate UserType , EclipseLink Converter etc). (la deuxième solution). ~ou~
  • Vous devrez utiliser l'astuce @PrePersist et @PostLoad (la première solution). ~ou~
  • Annotez les getter et setter qui prennent et renvoient les éléments suivants int valeur ~ou~
  • Utilisez un attribut entier au niveau de l'entité et effectuez une traduction dans les getters et setters.

Je vais illustrer la dernière option (il s'agit d'une mise en œuvre 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();
    }

}

52 votes

C'est dommage que JPA n'ait pas de support natif pour cela.

22 votes

@jaime D'accord ! Est-il fou de penser que les développeurs pourraient vouloir faire persister un enum comme la valeur d'un de ses champs/propriétés au lieu de sa valeur int ou de son nom ? Ces deux éléments sont extrêmement "fragiles" et peu propices au remaniement. Et l'utilisation du nom suppose également que vous utilisez la même convention de dénomination à la fois dans Java et dans la base de données. Prenons l'exemple du sexe. Il peut être défini comme étant simplement "M" ou "F" dans la base de données, mais cela ne devrait pas m'empêcher d'utiliser Gender.MALE et Gender.FEMALE en Java, au lieu de Gender.M ou Gender.F.

3 votes

Je suppose qu'une raison pourrait être que le nom et l'ordinal sont tous deux garantis comme étant uniques, alors que les autres valeurs ou champs ne le sont pas. Il est vrai que l'ordre pourrait changer (n'utilisez pas l'ordinal) et que le nom pourrait également être modifié (ne changez pas les noms d'enum :P), mais il en va de même pour toute autre valeur... Je ne suis pas sûr de voir la grande valeur de l'ajout de la possibilité de stocker une valeur différente.

87voto

Tvaroh Points 666

Cela est désormais possible avec JPA 2.1 :

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

Plus de détails :

2 votes

Qu'est-ce qui est maintenant possible ? Bien sûr, nous pouvons utiliser @Converter mais enum devraient être traitées de manière plus élégante dès la sortie de la boîte !

4 votes

"La réponse renvoie à deux liens qui parlent de l'utilisation d'un système de gestion de l'information. AttributeConverter MAIS cite un code qui ne fait rien de tel et ne répond pas à l'OP.

0 votes

@DN1 n'hésitez pas à l'améliorer

10voto

JoD. Points 188

Le problème est, je pense, que JPA n'a jamais été conçu avec l'idée en tête que nous pourrions avoir un schéma complexe préexistant déjà en place.

Je pense qu'il en résulte deux défauts principaux, spécifiques aux Enum :

  1. La limitation de l'utilisation de name() et ordinal(). Pourquoi ne pas simplement marquer un getter avec @Id, comme nous le faisons avec @Entity ?
  2. Les Enum ont généralement une représentation dans la base de données pour permettre l'association avec 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 Enum combinée à la flexibilité d'une Entity.

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

4voto

Rafiq Points 1060

Code apparenté possiblement proche 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();
    }

}

3voto

Carlos Cariello Points 11

Je ferais ce qui suit :

Déclarez séparément l'enum, 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 Droit

@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 seulement et il y a un problème que je ne discuterai pas ici avec ces enum's pour être directement persisté en utilisant le convertisseur, donc ce sera une route à sens unique seulement)

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é 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 de l'enum. Cependant, vous pouvez définir le champ Droit dans Autorité en utilisant

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 totalement correct, puisque le convertisseur n'est pas destiné à être utilisé avec les enum. En fait, la documentation dit de ne jamais l'utiliser dans ce but, vous devriez utiliser l'annotation @Enumerated à la place. Le problème est qu'il n'existe que deux types d'enum : ORDINAL ou STRING, mais le ORDINAL est délicat et n'est pas sûr.


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

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é 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 deuxième 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 le EnumType.ID où le champ de valeur de l'enum devrait être annoté avec une sorte d'annotation @EnumId.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X