java mapear - ¿Mapa enum en JPA con valores fijos?





persistir hibernate (7)


El problema es que, creo, que JPA nunca tuvo en cuenta la idea en mente de que podríamos tener un esquema preexistente complejo ya en su lugar.

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

  1. La limitación de usar name () y ordinal (). ¿Por qué no solo marca un getter con @Id, de la misma manera que hacemos con @Entity?
  2. Enum generalmente tienen representación en la base de datos para permitir la asociación con todo tipo de metadatos, incluyendo 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.

Ayuda a mi causa y vota en JPA_SPEC-47

¿No sería 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;
  }
}

Estoy buscando diferentes formas de mapear una enumeración usando JPA. Especialmente quiero establecer el valor entero de cada entrada enum 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 enum (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 enum 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:

  • utilice los métodos setter y getter del miembro correcto de la Clase de autoridad al cargar y guardar el objeto Authority
  • una idea equivalente sería decirle a JPA cuáles son los métodos de Enum correcto para convertir enum a int y int a enum
  • Debido a que estoy usando Spring, ¿hay alguna manera de decirle a JPA que use un convertidor específico (RightEditor)?



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

}



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 que persista directamente utilizando 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 la 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 genial, creo. No es del todo correcto, ya que el convertidor no está destinado a ser utilizado con enum's. En realidad, la documentación dice que nunca la use para este propósito; en su lugar, debe usar la anotación @Enumerated. El problema es que solo hay dos tipos de enum: ORDINAL o STRING, pero el 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 de la 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 enum debe ser anotado con algún tipo de anotación @EnumId.




Desde JPA 2.1 puede usar AttributeConverter .

Crea una clase enumerada como esta:

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 que solo necesitas:

@Column(name = "node_type_code")

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




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 post 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 de mapear enumeraciones dentro de sus entidades JPA usando la anotación @Enumerated. Desafortunadamente tanto EnumType.STRING como EnumType.ORDINAL tienen sus limitaciones.

Si usa EnumType.String, el cambio de nombre de uno de sus tipos enum hará que su valor enum no esté sincronizado con los valores guardados en la base de datos. Si usa EnumType.ORDINAL, eliminar o reordenar los tipos dentro de su enumeración hará que los valores guardados en la base de datos se correlacionen con los tipos de enumeraciones 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 riesgo la integridad de sus datos.

3. Retrollamadas del ciclo de vida

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

4. Mapeo de identificación única para 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 predefinido y fijo 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, usted 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 como oposición al uso de 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 correlaciona la enumeración con la base de datos.

Vea la post original para el ejemplo de código.







El quid de la cuestión es que la palabra referencia en la expresión "pasar por referencia" significa algo completamente diferente del significado habitual de la palabra referencia en Java.

Por lo general, en referencia de Java significa una referencia a un objeto . Pero los términos técnicos que pasan por referencia / valor de la teoría del lenguaje de programación están hablando de una referencia a la celda de memoria que contiene la variable , que es algo completamente diferente.





java spring orm jpa enums