Mapper une énumération dans JPA avec des valeurs fixes?

192

Je recherche les différentes façons de mapper une énumération à l'aide de JPA. Je souhaite en particulier définir la valeur entière de chaque entrée d'é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 d'énumération (0,1,2) et non la valeur que je veux (100,200,300).

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

Première solution

Une solution, proposée ici , utilise @PrePersist et @PostLoad pour convertir l'énumération 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ée ici proposait un objet de conversion générique, mais semble toujours 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")
            }
)

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 l'enregistrement de l'objet Authority
  • 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
  • Étant donné que j'utilise Spring, existe-t-il un moyen de dire à JPA d'utiliser un convertisseur spécifique (RightEditor)?
Kartoch
la source
7
Il est étrange d'utiliser ORDINAL, quelqu'un changera parfois de place des éléments dans l'énumération et la base de données deviendra un désastre
Natasha KP
2
cela ne s'appliquerait pas à l'utilisation de Name - quelqu'un peut changer le (s) nom (s) d'énumération et ils sont à nouveau désynchronisés avec la base de données ...
topchef
2
Je suis d'accord avec @NatashaKP. N'utilisez pas d'ordinal. Pour changer le nom, il n'y a rien de tel. Vous supprimez en fait l'ancienne énumération et en ajoutez une nouvelle avec un nouveau nom, donc oui, toutes les données stockées seraient désynchronisées (sémantique, peut-être: P).
Svend Hansen du
Oui, il y a 5 solutions que je connais. Voir ma réponse ci-dessous, où j'ai une réponse détaillée.
Chris Ritchie

Réponses:

168

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

  • 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 ~
  • Annoter getter et setter prenant et renvoyant la intvaleur ~ 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, ajustez-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();
    }

}
Pascal Thivent
la source
50
Si triste JPA n'a pas de support natif pour cela
Jaime Hablutzel
20
@jaime d'accord! Est-il fou de penser que les développeurs pourraient vouloir conserver une énumération comme valeur de l'un de ses champs / propriétés au lieu de sa valeur ou de son nom int? Ces deux éléments sont extrêmement «fragiles» et peu favorables à la refactorisation. Et l'utilisation du nom suppose également que vous utilisez la même convention de dénomination dans Java et dans la base de données. Prenons le genre par exemple. Cela pourrait être défini comme 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.
spaaarky21 du
2
Je suppose qu'une raison pourrait être que le nom et l'ordinal sont tous deux garantis uniques, alors que les autres valeurs ou champs ne le sont pas. Il est vrai que l'ordre pourrait changer (n'utilisez pas d'ordinal) et le nom pourrait également être changé (ne changez pas les noms d'énumération: P), mais toute autre valeur pourrait également ... Je ne suis pas sûr de voir la grande valeur avec l'ajout de la possibilité de stocker une valeur différente.
Svend Hansen
2
En fait, je vois la valeur ... Je supprimerais cette partie de mon commentaire ci-dessus, si je pouvais toujours le modifier: P: D
Svend Hansen
13
JPA2.1 aura le support du convertisseur nativement. S'il vous plaît voir Somethoughtsonjava.blogspot.fi/2013/10/…
drodil
69

C'est désormais possible avec JPA 2.1:

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

Plus de détails:

Dave Jarvis
la source
2
Qu'est-ce qui est maintenant possible? Bien sûr, nous pouvons utiliser @Converter, mais enumdevrait être manipulé plus élégamment dès la sortie de la boîte!
YoYo
4
"Answer" fait référence à 2 liens qui parlent d'utiliser un AttributeConverterMAIS cite du code qui ne fait rien de tel et ne répond pas à l'OP.
@ DN1 n'hésitez pas à l'améliorer
Tvaroh
1
C'est votre "réponse", vous devriez donc "l'améliorer". Il y a déjà une réponse pourAttributeConverter
1
@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Fonctionne
23

Depuis 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 selon le conteneur, mais testé pour fonctionner sur Wildfly 8.1.0. Si cela ne fonctionne pas, vous pouvez ajouter @Convert(converter = NodeTypeConverter.class)la colonne de classe d'entité.

bassin
la source
"values ​​()" doit être "NodeType.values ​​()"
Curtis Yallop
17

La meilleure approche serait de mapper un identifiant unique à chaque type d'énumération, évitant ainsi les pièges de ORDINAL et STRING. Voir ce post qui décrit 5 façons de mapper une énumération.

Tiré du lien ci-dessus:

1 et 2. Utilisation de @Enumerated

Il existe actuellement 2 façons de mapper des énumérations dans vos entités JPA à l'aide de l'annotation @Enumerated. Malheureusement, EnumType.STRING et EnumType.ORDINAL ont leurs limites.

Si vous utilisez EnumType.String, renommer l'un de vos types d'énumération entraînera la désynchronisation de votre valeur d'énumération avec les valeurs enregistrées dans la base de données. Si vous utilisez EnumType.ORDINAL, la suppression ou la réorganisation des types dans votre énumération entraînera le mappage des valeurs enregistrées dans la base de données vers les types d'énumérations incorrects.

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

3. Rappels de cycle de vie

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

4. Mappage de l'ID unique à chaque type d'énumération

La solution préférée consiste à mapper votre énumération à une valeur fixe, ou ID, définie dans l'énumération. Le mappage à une valeur fixe prédéfinie rend votre code plus robuste. Toute modification de l'ordre des types d'énumérations, ou la refactorisation des noms, n'entraînera aucun effet indésirable.

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 enregistrées dans la base de données pour chaque type d'énumération. Dans votre entité, vous annoteriez ensuite votre énumération avec @Convert.

Ma préférence: (Numéro 4)

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

Voir l'original après pour l'exemple de code.

Chris Ritchie
la source
1
javax.persistence.Converter est simple et avec @Converter (autoApply = true), il permet de garder les classes de domaine exemptes d'annotations @Convert
Rostislav Matl
9

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

Je pense qu'il y a deux lacunes principales qui en résultent, spécifiques à 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 une localisation, etc. Nous avons besoin de la facilité d'utilisation d'une Enum combinée à la flexibilité d'une Entité.

Aidez ma cause et votez sur JPA_SPEC-47

Ne serait-ce 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;
  }
}
JoD.
la source
4

Code lié éventuellement 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();
    }

}
Rafiq
la source
3

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 y a un problème que je ne discuterai pas ici avec ces énumérations pour être directement persistant en utilisant le convertisseur, donc ce sera une route à sens unique)

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;

}

En procédant de cette manière, vous ne pouvez pas définir directement le champ 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, car le convertisseur n'est pas destiné à être utilisé avec les enum. En fait, la documentation dit de ne jamais l'utiliser à cette fin, vous devriez utiliser l'annotation @Enumerated à la place. Le problème est qu'il n'y a que deux types d'énumérations: ORDINAL ou STRING, mais ORDINAL est délicat et dangereux.


Cependant, si cela ne vous satisfait pas, vous pouvez faire quelque chose d'un peu plus hacky 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 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 le EnumType.ID où le champ de valeur enum devrait être annoté avec une sorte d'annotation @EnumId.

Carlos Cariello
la source
2

Ma propre solution pour résoudre ce type de mappage Enum JPA est la suivante.

Étape 1 - Écrivez l'interface suivante que nous utiliserons pour toutes les énumérations que nous voulons mapper à une colonne db:

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

    T getDbVal();

}

Étape 2 - Implémentez un convertisseur JPA générique personnalisé comme suit:

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

}

Cette classe convertira la valeur enum Een un champ de base de données de type T(par exemple String) en utilisant getDbVal()on enum E, et vice versa.

Étape 3 - Laissez l'énumération d'origine implémenter l'interface que nous avons définie à l'étape 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;
    }
}

Étape 4 - Étendez le convertisseur de l'étape 2 pour l' Righténumération de l'étape 3:

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

Étape 5 - La dernière étape consiste à annoter le champ dans l'entité comme suit:

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

Conclusion

À mon humble avis, c'est la solution la plus propre et la plus élégante si vous avez de nombreuses énumérations à mapper et que vous souhaitez utiliser un champ particulier de l'énumération elle-même comme valeur de mappage.

Pour toutes les autres énumérations de votre projet qui nécessitent une logique de mappage similaire, il vous suffit de répéter les étapes 3 à 5, c'est-à-dire:

  • implémentez l'interface IDbValuesur votre énumération;
  • étendre le EnumDbValueConverter avec seulement 3 lignes de code (vous pouvez également le faire dans votre entité pour éviter de créer une classe séparée);
  • annotez l'attribut enum avec @Convertfrom javax.persistencepackage.

J'espère que cela t'aides.

Danilo Cianciulli
la source
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

premier cas ( EnumType.ORDINAL)

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

deuxième cas ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Mahdi Ben Selimene
la source