Mappa enum in JPA con valori fissi?


192

Sto cercando i diversi modi per mappare un enum usando JPA. In particolare, voglio impostare il valore intero di ciascuna voce enum e salvare solo il valore intero.

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

Una soluzione semplice è utilizzare l'annotazione enumerata con EnumType.ORDINAL:

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

Ma in questo caso JPA mappa l'indice enum (0,1,2) e non il valore che desidero (100.200.300).

Le due soluzioni che ho trovato non sembrano semplici ...

Prima soluzione

Una soluzione, qui proposta , utilizza @PrePersist e @PostLoad per convertire l'enum in un altro campo e contrassegnare il campo enum come transitorio:

@Basic
private int intValueForAnEnum;

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

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

Seconda soluzione

La seconda soluzione proposta qui proponeva un oggetto di conversione generico, ma sembra ancora pesante e orientato all'ibernazione (@Type non sembra esistere in 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")
            }
)

Esistono altre soluzioni?

Ho in mente diverse idee ma non so se esistono nell'APP:

  • utilizzare i metodi setter e getter del membro destro della classe di autorità durante il caricamento e il salvataggio dell'oggetto Authority
  • un'idea equivalente sarebbe quella di dire all'APP quali sono i metodi di Right enum per convertire enum in int e int in enum
  • Dato che sto usando Spring, c'è un modo per dire a JPA di usare un convertitore specifico (RightEditor)?

7
È strano usare ORDINAL a volte qualcuno cambierà i punti degli oggetti nell'enumerazione e il database diventerà un disastro
Natasha KP

2
lo stesso non si applicherebbe all'utilizzo di Name - qualcuno potrebbe cambiare i nomi di enum e sono di nuovo fuori sincrono con il database ...
topchef

2
Sono d'accordo con @NatashaKP. Non usare ordinale. Per aver cambiato il nome, non esiste nulla del genere. Stai effettivamente eliminando il vecchio enum e ne aggiungi uno nuovo con un nuovo nome, quindi sì, tutti i dati memorizzati sarebbero fuori sincrono (semantica, forse: P).
Svend Hansen,

Sì, ci sono 5 soluzioni che conosco. Vedi la mia risposta di seguito, dove ho una risposta dettagliata.
Chris Ritchie, il

Risposte:


168

Per le versioni precedenti a JPA 2.1, JPA offre solo due modi per gestire gli enum, con i loro nameo con i loro ordinal. E l'APP standard non supporta tipi personalizzati. Così:

  • Se desideri effettuare conversioni di tipo personalizzate, dovrai utilizzare un'estensione del provider (con Hibernate UserType, EclipseLink Converter, ecc.). (la seconda soluzione). ~ o ~
  • Dovrai usare il trucco @PrePersist e @PostLoad (la prima soluzione). ~ O ~
  • Annota getter e setter prendendo e restituendo il intvalore ~ ​​o ~
  • Utilizzare un attributo intero a livello di entità ed eseguire una traduzione in getter e setter.

Illustrerò l'ultima opzione (questa è un'implementazione di base, modificala come richiesto):

@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
L'APP così triste non ha supporto nativo per questo
Jaime Hablutzel,

20
@jaime Agreed! È folle pensare che gli sviluppatori potrebbero voler perseguire un enum come valore di uno dei suoi campi / proprietà anziché il suo valore o nome int? Entrambi sono estremamente "fragili" e ostili al refactoring. Inoltre, l'utilizzo del nome presuppone che si stia utilizzando la stessa convenzione di denominazione sia in Java che nel database. Prendi il genere per esempio. Ciò potrebbe essere definito semplicemente come "M" o "F" nel database, ma ciò non dovrebbe impedirmi di utilizzare Gender.MALE e Gender.FEMALE in Java, anziché Gender.M o Gender.F.
spaaarky21,

2
Immagino che una ragione potrebbe essere che il nome e l'ordinale sono entrambi garantiti come univoci, mentre altri valori o campi non lo sono. È vero che l'ordine potrebbe cambiare (non usare l'ordinale) e anche il nome potrebbe essere cambiato (non cambiare i nomi enum: P), ma così potrebbe fare qualsiasi altro valore ... Non sono sicuro di vedere l'ottimo valore con l'aggiunta della possibilità di memorizzare un valore diverso.
Svend Hansen,

2
In realtà, vedo il valore ... Eliminerei quella parte del mio commento sopra, se potessi ancora modificarlo: P: D
Svend Hansen

13
JPA2.1 avrà il supporto del convertitore in modo nativo. Si prega di consultare somethoughtsonjava.blogspot.fi/2013/10/…
drodil

69

Questo è ora possibile con JPA 2.1:

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

Maggiori dettagli:


2
Cosa è ora possibile? Sicuro che possiamo usare @Converter, ma enumdovrebbe essere gestito in modo più elegante fuori dalla scatola!
YoYo,

4
"Rispondi" fa riferimento a 2 collegamenti che parlano dell'utilizzo di AttributeConverterMA, ma cita un codice che non fa nulla del genere e non risponde all'OP.

@ DN1 sentiti libero di migliorarlo
Tvaroh

1
È la tua "risposta", quindi dovresti "migliorarla". C'è già una risposta perAttributeConverter

1
Perfettamente funzionante dopo aver aggiunto:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28

23

Da JPA 2.1 è possibile utilizzare AttributeConverter .

Crea una classe enumerata in questo modo:

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

E crea un convertitore come questo:

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

Sull'entità hai solo bisogno di:

@Column(name = "node_type_code")

La tua fortuna @Converter(autoApply = true)può variare in base al contenitore ma testato per funzionare su Wildfly 8.1.0. Se non funziona, puoi aggiungerlo @Convert(converter = NodeTypeConverter.class)nella colonna della classe di entità.


"valori ()" dovrebbe essere "NodeType.values ​​()"
Curtis Yallop

17

L'approccio migliore sarebbe mappare un ID univoco per ogni tipo di enum, evitando così le insidie ​​di ORDINAL e STRING. Vedi questo post che delinea 5 modi in cui puoi mappare un enum.

Tratto dal link sopra:

1 & 2. Utilizzando @Enumerated

Attualmente ci sono 2 modi in cui è possibile mappare gli enum all'interno delle entità JPA usando l'annotazione @Enumerated. Sfortunatamente sia EnumType.STRING che EnumType.ORDINAL hanno i loro limiti.

Se si utilizza EnumType.String, la ridenominazione di uno dei tipi di enum renderà il valore del enum non sincronizzato con i valori salvati nel database. Se si utilizza EnumType.ORDINAL, l'eliminazione o il riordino dei tipi all'interno del proprio enum farà sì che i valori salvati nel database vengano mappati sui tipi di enum errati.

Entrambe queste opzioni sono fragili. Se l'enum viene modificato senza eseguire una migrazione del database, è possibile compromettere l'integrità dei dati.

3. Richiamate del ciclo di vita

Una possibile soluzione sarebbe quella di utilizzare le annotazioni di richiamata del ciclo di vita di JPA, @PrePersist e @PostLoad. Questo sembra abbastanza brutto poiché ora avrai due variabili nella tua entità. Uno mappando il valore archiviato nel database e l'altro, l'enum effettivo.

4. Mappatura dell'ID univoco su ciascun tipo di enum

La soluzione preferita è mappare il tuo enum su un valore fisso, o ID, definito all'interno del enum. La mappatura su un valore predefinito predefinito rende il codice più robusto. Qualsiasi modifica all'ordine dei tipi di enum, o il refactoring dei nomi, non causerà alcun effetto negativo.

5. Utilizzo di Java EE7 @Convert

Se si utilizza JPA 2.1, è possibile utilizzare la nuova annotazione @Convert. Ciò richiede la creazione di una classe di convertitore, annotata con @Converter, all'interno della quale definire i valori salvati nel database per ciascun tipo di enum. All'interno della tua entità dovresti quindi annotare la tua enum con @Convert.

Le mie preferenze: (Numero 4)

Il motivo per cui preferisco definire i miei ID all'interno dell'enum invece di utilizzare un convertitore, è un buon incapsulamento. Solo il tipo enum dovrebbe conoscere il proprio ID e solo l'entità dovrebbe sapere come associa l'enum al database.

Vedi il post originale per l'esempio di codice.


1
javax.persistence.Converter è semplice e con @Converter (autoApply = true) consente di mantenere le classi di dominio libere dalle annotazioni di @Convert
Rostislav Matl,

9

Il problema è, penso, che l'APP non è mai stato accolto con l'idea in mente che potremmo avere già uno schema preesistente complesso in atto.

Penso che ci siano due principali carenze risultanti da questo, specifiche di Enum:

  1. La limitazione dell'uso di name () e ordinal (). Perché non contrassegnare semplicemente un getter con @Id, come facciamo con @Entity?
  2. Gli Enum di solito hanno una rappresentazione nel database per consentire l'associazione con tutti i tipi di metadati, incluso un nome proprio, un nome descrittivo, forse qualcosa con localizzazione ecc. Abbiamo bisogno della facilità d'uso di un Enum unita alla flessibilità di un'entità.

Aiuta la mia causa e vota su JPA_SPEC-47

Non sarebbe più elegante dell'utilizzo di un @Converter per risolvere il problema?

// 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

Possibilmente vicino il codice correlato di 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

Vorrei fare il folowing:

Dichiarare separatamente l'enum, nel suo file:

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


}

Dichiarare una nuova entità JPA denominata 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);
    }


}

Avrai anche bisogno di un convertitore per ricevere questi valori (solo JPA 2.1 e c'è un problema che non discuterò qui con questi enum che devono essere perseverati direttamente usando il convertitore, quindi sarà solo una strada a senso unico)

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à dell'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;

}

In questo modo, non è possibile impostare direttamente sul campo enum. Tuttavia, è possibile impostare il campo destro in Autorità utilizzando

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

E se devi confrontare, puoi usare:

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

Il che è piuttosto interessante, penso. Non è del tutto corretto, dal momento che il convertitore non è progettato per essere utilizzato con Enum. In realtà, la documentazione dice di non usarlo mai per questo scopo, dovresti invece usare l'annotazione @Enumerated. Il problema è che esistono solo due tipi di enum: ORDINAL o STRING, ma ORDINAL è complicato e non sicuro.


Tuttavia, se non ti soddisfa, puoi fare qualcosa di un po 'più confuso e più semplice (o meno).

Vediamo.

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


}

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

}

In questa seconda idea, non è una situazione perfetta da quando abbiamo hackerato l'attributo ordinale, ma è una codifica molto più piccola.

Penso che la specifica JPA dovrebbe includere EnumType.ID in cui il campo del valore enum dovrebbe essere annotato con una sorta di annotazione @EnumId.


2

La mia soluzione per risolvere questo tipo di mappatura Enum JPA è la seguente.

Step 1 : Scrivi la seguente interfaccia che useremo per tutti gli enumerati che vogliamo mappare su una colonna db:

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

    T getDbVal();

}

Passaggio 2 : implementare un convertitore JPA generico personalizzato come segue:

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

}

Questa classe convertirà il valore enum Ein un campo di database di tipo T(ad es. String) Usando getDbVal()on enumE e viceversa.

Passaggio 3 : consentire all'enum originale di implementare l'interfaccia definita nel passaggio 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;
    }
}

Passaggio 4 : estendere il convertitore del passaggio 2 per l' Rightenum del passaggio 3:

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

Passaggio 5 : il passaggio finale consiste nell'annotare il campo nell'entità come segue:

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

Conclusione

IMHO questa è la soluzione più pulita ed elegante se hai molti enum da mappare e vuoi usare un particolare campo dell'enum come valore di mappatura.

Per tutte le altre enumerazioni nel tuo progetto che richiedono una logica di mappatura simile, devi solo ripetere i passaggi da 3 a 5, ovvero:

  • implementa l'interfaccia IDbValuesul tuo enum;
  • estendere la EnumDbValueConverter con solo 3 righe di codice (puoi anche farlo all'interno della tua entità per evitare di creare una classe separata);
  • annota l'attributo enum con @Convertdal javax.persistencepacchetto.

Spero che questo ti aiuti.


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

primo caso ( EnumType.ORDINAL)

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

secondo caso ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.