Map enum en JPA con valores fijos?


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 mapea 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í , usa @PrePersist y @PostLoad para convertir la enumeración en otro campo y marcar el campo 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 todavía parece pesado y orientado a 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 se si existen en JPA:

  • utilice el métodos setter y getter del miembro correcto de la Clase Authority al cargar y guardar el objeto Authority
  • 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 manera de decirle a JPA que use un convertidor específico (RightEditor) ?
Author: Arjan Tijms, 2010-05-02

7 answers

Para versiones anteriores a JPA 2.1, JPA proporciona solo dos formas de tratar con enums, por su name o por su ordinal. Y el JPA estándar no admite tipos personalizados. Entonces:

  • Si desea realizar conversiones de tipo personalizado, tendrá que usar una extensión de proveedor (con Hibernate UserType, EclipseLink Converter, etc.). (la segunda solución). ~o~
  • Tendrás que usar el truco @PrePersist y @PostLoad (la primera solución). ~o~
  • Anotar getter y setter tomando y devolver el valor int ~o~
  • Utilice un atributo entero a nivel de entidad y realice una traducción en getters y setters.

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

}
 153
Author: Pascal Thivent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-12 21:48:35

Esto ahora es posible con JPA 2.1:

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

Más detalles:

 57
Author: Dave Jarvis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-12 21:45:17

El mejor enfoque sería asignar un ID único a cada tipo de enumeración, evitando así las trampas de ORDINAL y STRING. Vea este post que describe 5 formas en que puede mapear una enumeración.

Tomado del enlace de arriba:

1&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 y EnumType.ORDINAL tienen sus limitaciones.

Si usa EnumType.String luego 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 luego eliminar o reordenar los tipos dentro de su enumeración hará que los valores guardados en la base de datos se asignen a los tipos de enumeración incorrectos.

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

3. Lifecycle Callbacks

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

4. Asignación de ID único 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 en el 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, entonces anotaría su enumeración con @Convertir.

Mi preferencia: (Número 4)

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

Ver el original post para el ejemplo de código.

 15
Author: Chris Ritchie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-26 11:32:02

El problema es, creo, que JPA nunca se inició con la idea en mente de que podríamos tener un Esquema preexistente complejo ya en su lugar.

Creo que hay dos deficiencias principales resultantes de esto, específicas de Enum:

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

Ayuda a mi causa y vota 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;
  }
}
 10
Author: JoD.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-26 01:32:30

Desde JPA 2.1 puedes usar AttributeConverter.

Crea 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 : 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")

Su suerte con @Converter(autoApply = true) puede variar según el contenedor, pero probado para funcionar en Wildfly 8.1.0. Si no funciona, puede agregar @Convert(converter = NodeTypeConverter.class) en la columna entity class.

 5
Author: Pool,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-11 15:32:56

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

}
 3
Author: Rafiq,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-31 08:43:52

Haría lo siguiente:

Declarar separadamente 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 que se persistirán directamente usando el convertidor, por lo que será un camino de una sola dirección)

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 de 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 enumeración. Sin embargo, puede establecer el campo correcto en Autoridad usando

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

Y si necesitas comparar, puedes usar:

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

Lo cual es bastante genial, creo. No es totalmente correcto, ya que el convertidor no está destinado a ser utilizado con enumeraciones. 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 enum: ORDINAL o STRING, pero el ORDINAL es complicado y no seguro.


Sin embargo, si no te satisface, puedes hacer algo un poco más hackeado y 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 hackeamos el atributo ordinal, pero es una codificación mucho más pequeña.

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

 2
Author: Carlos Cariello,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-22 12:02:59