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 ...
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);
}
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")
}
)
J'ai plusieurs idées en tête mais je ne sais pas si elles existent dans JPA:
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:
UserType
, EclipseLink Converter
, etc.). (la deuxième solution). ~ ou ~int
valeur ~ ou ~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();
}
}
C'est désormais possible avec JPA 2.1:
@Column(name = "RIGHT")
@Enumerated(EnumType.STRING)
private Right right;
Plus de détails:
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.
À 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é.
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:
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;
}
}
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();
}
}
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.