persistir mapear enumerated enum annotation java spring orm jpa enums

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



mapear enum jpa (7)

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.

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)?

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



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

  • Si desea realizar conversiones de tipo personalizado, deberá 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 devolviendo el valor int ~ o ~
  • Use un atributo entero en el 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 se requiera):

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


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.