Mapear enumeración en JPA con valores fijos?

192

Estoy buscando las diferentes formas de mapear una enumeración usando JPA. Especialmente quiero establecer el valor entero de cada entrada de enumeración y guardar solo el valor entero.

@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 solución simple es usar la anotación enumerada con EnumType.ORDINAL:

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

Pero en este caso, JPA asigna el índice de enumeración (0,1,2) y no el valor que quiero (100,200,300).

Las dos soluciones que encontré no parecen simples ...

Primera solución

Una solución, propuesta aquí , utiliza @PrePersist y @PostLoad para convertir la enumeración a otro campo y marcar el campo de enumeración como transitorio:

@Basic
private int intValueForAnEnum;

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

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

Segunda solución

La segunda solución propuesta aquí propuso un objeto de conversión genérico, pero aún parece pesado y orientado a la hibernación (@Type no parece existir 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")
            }
)

¿Hay alguna otra solución?

Tengo varias ideas en mente pero no sé si existen en JPA:

  • use los métodos setter y getter del miembro correcto de la clase de autoridad al cargar y guardar el objeto de autoridad
  • una idea equivalente sería decirle a JPA cuáles son los métodos de Right enum para convertir enum a int e int a enum
  • Debido a que estoy usando Spring, ¿hay alguna forma de decirle a JPA que use un convertidor específico (RightEditor)?
Kartoch
fuente
77
Es extraño usar ORDINAL, alguien a veces cambiará los lugares de los elementos en la enumeración y la base de datos se convertirá en un desastre
Natasha KP
2
no se aplicaría lo mismo al uso de Name: alguien puede cambiar los nombres de enumeración y de nuevo no están sincronizados con la base de datos ...
topchef 03 de
2
Estoy de acuerdo con @NatashaKP. No uses ordinal. Para cambiar el nombre, no existe tal cosa. En realidad, está eliminando la enumeración anterior y agregando una nueva con un nombre nuevo, por lo que sí, cualquier dato almacenado no estará sincronizado (semántica, tal vez: P).
Svend Hansen
Sí, hay 5 soluciones que conozco. Vea mi respuesta a continuación, donde tengo una respuesta detallada.
Chris Ritchie

Respuestas:

168

Para versiones anteriores a JPA 2.1, JPA proporciona sólo dos maneras de lidiar con las enumeraciones, por su nameo por su ordinal. Y el JPA estándar no admite tipos personalizados. Entonces:

  • Si desea realizar conversiones de tipo personalizado, deberá utilizar una extensión de proveedor (con Hibernate UserType, EclipseLink Converter, etc.). (La segunda solución). ~ o ~
  • Tendrá que usar el truco @PrePersist y @PostLoad (la primera solución). ~ o ~
  • Anotar getter y setter tomando y devolviendo el intvalor ~ o ~
  • Utilice un atributo entero a nivel de entidad y realice una traducción en captadores y definidores.

Ilustraré la última opción (esta es una implementación básica, modifíquela según sea necesario):

@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
fuente
50
Tan triste JPA no tiene soporte nativo para esto
Jaime Hablutzel
20
@jaime De acuerdo! ¿Es una locura pensar que los desarrolladores podrían querer persistir una enumeración como el valor de uno de sus campos / propiedades en lugar de su valor o nombre int? Ambos son extremadamente "frágiles" y refactorizadores, hostiles. Y usar el nombre también supone que está usando la misma convención de nomenclatura tanto en Java como en la base de datos. Tome el género por ejemplo. Eso podría definirse simplemente como 'M' o 'F' en la base de datos, pero eso no debería impedirme usar Gender.MALE y Gender.FEMALE en Java, en lugar de Gender.M o Gender.F.
spaaarky21
2
Supongo que una razón podría ser que el nombre y el ordinal están garantizados como únicos, mientras que cualquier otro valor o campo no lo es. Es cierto que el orden podría cambiar (no use ordinal) y el nombre también podría cambiarse (no cambie los nombres de enumeración: P), pero también podría cambiar cualquier otro valor ... No estoy seguro de ver El gran valor al agregar la capacidad de almacenar un valor diferente.
Svend Hansen
2
En realidad, veo el valor ... Eliminaría esa parte de mi comentario anterior, si aún pudiera editarlo: P: D
Svend Hansen
13
JPA2.1 tendrá el convertidor compatible de forma nativa. Por favor, vea somethoughtsonjava.blogspot.fi/2013/10/…
drodil
69

Esto ahora es posible con JPA 2.1:

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

Más detalles:

Dave Jarvis
fuente
2
¿Qué es ahora posible? Claro que podemos usar @Converter, ¡pero enumdebería manejarse de manera más elegante fuera de la caja!
YoYo
44
"Respuesta" hace referencia a 2 enlaces que hablan de usar un AttributeConverterPERO cita algún código que no hace nada por el estilo y no responde al OP.
@ DN1 siéntase libre de mejorarlo
Tvaroh
1
Es su "respuesta", por lo que debe "mejorarla". Ya hay una respuesta paraAttributeConverter
1
Perfectamente trabajando después de agregar:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28
23

Desde JPA 2.1 puede usar AttributeConverter .

Cree una clase enumerada así:

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

Y crea un convertidor como este:

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

En la entidad solo necesitas:

@Column(name = "node_type_code")

@Converter(autoApply = true)Su suerte puede variar según el contenedor, pero se probó que funciona en Wildfly 8.1.0. Si no funciona, puede agregar @Convert(converter = NodeTypeConverter.class)en la columna de clase de entidad.

Piscina
fuente
"valores ()" debe ser "NodeType.values ​​()"
Curtis Yallop
17

El mejor enfoque sería mapear una ID única para cada tipo de enumeración, evitando así las trampas de ORDINAL y STRING. Vea esta publicación que describe 5 formas en que puede mapear una enumeración.

Tomado del enlace de arriba:

1 y 2. Usando @Enumerated

Actualmente hay 2 formas en que puede asignar enumeraciones dentro de sus entidades JPA utilizando la anotación @Enumerated. Desafortunadamente, tanto EnumType.STRING como EnumType.ORDINAL tienen sus limitaciones.

Si usa EnumType.String, cambiar el nombre de uno de sus tipos de enumeración hará que su valor de enumeración no esté sincronizado con los valores guardados en la base de datos. Si usa EnumType.ORDINAL, al eliminar o reordenar los tipos dentro de su enumeración, los valores guardados en la base de datos se correlacionarán con los tipos de enumeraciones incorrectos.

Ambas opciones son frágiles. Si se modifica la enumeración sin realizar una migración de la base de datos, podría poner en riesgo la integridad de sus datos.

3. Devolución del ciclo de vida

Una posible solución sería utilizar las anotaciones de devolución de llamadas del ciclo de vida de JPA, @PrePersist y @PostLoad. Esto se siente bastante feo ya que ahora tendrá dos variables en su entidad. Uno asigna el valor almacenado en la base de datos y el otro, la enumeración real.

4. Asignación de ID única a cada tipo de enumeración

La solución preferida es asignar su enumeración a un valor fijo, o ID, definido dentro de la enumeración. La asignación a un valor fijo predefinido hace que su código sea más robusto. Cualquier modificación al orden de los tipos de enumeraciones, o la refactorización de los nombres, no causará ningún efecto adverso.

5. Usando Java EE7 @Convert

Si está utilizando JPA 2.1, tiene la opción de usar la nueva anotación @Convert. Esto requiere la creación de una clase de convertidor, anotada con @Converter, dentro de la cual definiría qué valores se guardan en la base de datos para cada tipo de enumeración. Dentro de su entidad, anotaría su enumeración con @Convert.

Mi preferencia: (número 4)

La razón por la que prefiero definir mis ID dentro de la enumeración en lugar de usar un convertidor, es una buena encapsulación. Solo el tipo de enumeración debe conocer su ID, y solo la entidad debe saber cómo asigna la enumeración a la base de datos.

Ver la publicación original para el ejemplo de código.

Chris Ritchie
fuente
1
javax.persistence.Converter es simple y con @Converter (autoApply = true) permite mantener las clases de dominio libres de anotaciones de @Convert
Rostislav Matl
9

Creo que el problema es que JPA nunca fue concebido con la idea en mente de que podríamos tener un Esquema preexistente complejo ya establecido.

Creo que hay dos defectos principales que resultan de esto, específicos de Enum:

  1. La limitación de usar name () y ordinal (). ¿Por qué no simplemente marcar un getter con @Id, como lo hacemos con @Entity?
  2. Los Enum suelen tener representación en la base de datos para permitir la asociación con todo tipo de metadatos, incluido un nombre propio, un nombre descriptivo, tal vez algo con localización, etc. Necesitamos la facilidad de uso de un Enum combinado con la flexibilidad de una Entidad.

Ayude a mi causa y vote sobre JPA_SPEC-47

¿No sería esto más elegante que usar un @Converter para resolver el 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;
  }
}
JoD.
fuente
4

Posiblemente el código relacionado cercano 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
fuente
3

Yo haría lo siguiente:

Declare por separado la enumeración, en su propio archivo:

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


}

Declarar una nueva entidad JPA llamada 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);
    }


}

También necesitará un convertidor para recibir estos valores (solo JPA 2.1 y hay un problema que no discutiré aquí con estas enumeraciones para persistir directamente usando el convertidor, por lo que será un camino de un solo sentido)

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

}

La entidad Autoridad:

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

}

Al hacer esto, no puede establecer directamente en el campo enum. Sin embargo, puede establecer el campo Derecho en Autoridad usando

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

Y si necesita comparar, puede usar:

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

Lo cual es bastante bueno, creo. No es totalmente correcto, ya que el convertidor no está destinado a usarse con enum´s. En realidad, la documentación dice que nunca lo use para este propósito, debe usar la anotación @Enumerated en su lugar. El problema es que solo hay dos tipos de enumeración: ORDINAL o STRING, pero ORDINAL es complicado y no es seguro.


Sin embargo, si no te satisface, puedes hacer algo un poco más hacky y más simple (o no).

Veamos.

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


}

y la entidad Autoridad

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

}

En esta segunda idea, no es una situación perfecta ya que pirateamos el atributo ordinal, pero es una codificación mucho más pequeña.

Creo que la especificación JPA debe incluir el EnumType.ID donde el campo de valor de enumeración debe ser anotado con algún tipo de anotación @EnumId.

Carlos Cariello
fuente
2

Mi propia solución para resolver este tipo de mapeo Enum JPA es la siguiente.

Paso 1 : escriba la siguiente interfaz que usaremos para todas las enumeraciones que queremos asignar a una columna db:

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

    T getDbVal();

}

Paso 2 : implemente un convertidor JPA genérico personalizado de la siguiente manera:

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

}

Esta clase convertirá el valor enum Ea un campo de tipo de base de datos T(por ejemplo String) mediante el uso getDbVal()de enum E, y viceversa.

Paso 3 : deje que la enumeración original implemente la interfaz que definimos en el paso 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;
    }
}

Paso 4 : amplíe el convertidor del paso 2 para la Rightenumeración del paso 3:

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

Paso 5 : el paso final es anotar el campo en la entidad de la siguiente manera:

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

Conclusión

En mi humilde opinión, esta es la solución más limpia y elegante si tiene muchas enumeraciones para asignar y desea utilizar un campo particular de la enumeración como valor de asignación.

Para todas las otras enumeraciones en su proyecto que necesitan una lógica de mapeo similar, solo tiene que repetir los pasos 3 a 5, es decir:

  • implemente la interfaz IDbValueen su enumeración;
  • extienda el EnumDbValueConvertercon solo 3 líneas de código (también puede hacerlo dentro de su entidad para evitar crear una clase separada);
  • anote el atributo enum con @Convertfrom javax.persistencepackage.

Espero que esto ayude.

Danilo Cianciulli
fuente
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

primer caso ( EnumType.ORDINAL)

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

segundo caso ( EnumType.STRING)

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