Enum do mapa na JPA com valores fixos?

192

Estou procurando as diferentes maneiras de mapear uma enumeração usando JPA. Quero especialmente definir o valor inteiro de cada entrada de enum e salvar apenas o valor inteiro.

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

Uma solução simples é usar a anotação enumerada com EnumType.ORDINAL:

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

Mas, neste caso, o JPA mapeia o índice de enumeração (0,1,2) e não o valor que eu quero (100,200,300).

As duas soluções que encontrei não parecem simples ...

Primeira solução

Uma solução, proposta aqui , usa @PrePersist e @PostLoad para converter a enumeração em outro campo e marcar a enumeração como transitória:

@Basic
private int intValueForAnEnum;

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

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

Segunda solução

A segunda solução proposta aqui propôs um objeto de conversão genérico, mas ainda parece pesado e orientado a hibernação (o @Type não parece existir no 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")
            }
)

Existem outras soluções?

Tenho várias idéias em mente, mas não sei se elas existem no JPA:

  • use os métodos setter e getter do membro certo da classe Authority ao carregar e salvar o objeto Authority
  • uma idéia equivalente seria dizer à JPA quais são os métodos de enum Right para converter enum em int e int enum
  • Como estou usando o Spring, há alguma maneira de dizer ao JPA para usar um conversor específico (RightEditor)?
Kartoch
fonte
7
É estranho usar ORDINAL alguém, às vezes, mudar de lugar dos itens na enumeração e banco de dados se tornará um desastre
Natasha KP
2
não seria o mesmo se aplica ao uso de nome - alguém pode mudar o nome (s) enum e eles são novamente fora de sincronia com banco de dados ...
topchef
2
Eu concordo com @NatashaKP. Não use ordinal. Para mudar o nome, não existe. Você está realmente excluindo a enumeração antiga e adicionando uma nova com um novo nome; portanto, sim, todos os dados armazenados estariam fora de sincronia (semântica, talvez: P).
Svend Hansen
Sim, existem 5 soluções que eu conheço. Veja minha resposta abaixo, onde tenho uma resposta detalhada.
Chris Ritchie

Respostas:

168

Para versões anteriores ao JPA 2.1, o JPA fornece apenas duas maneiras de lidar com enumerações, por eles nameou por eles ordinal. E o JPA padrão não suporta tipos personalizados. Assim:

  • Se você quiser fazer conversões de tipo personalizadas, precisará usar uma extensão de provedor (com Hibernate UserType, EclipseLink Converter, etc). (a segunda solução). ~ ou ~
  • Você precisará usar o truque @PrePersist e @PostLoad (a primeira solução). ~ ou ~
  • Anote getter e setter pegando e retornando o intvalor ~ ou ~
  • Use um atributo inteiro no nível da entidade e execute uma conversão em getters e setters.

Ilustrarei a opção mais recente (esta é uma implementação básica, ajuste-a conforme necessário):

@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
fonte
50
Tão triste JPA não tem suporte nativo para isso
Jaime Hablutzel
20
@jaime concordou! É louco pensar que os desenvolvedores podem querer manter uma enumeração como o valor de um de seus campos / propriedades, em vez de seu valor ou nome int? Ambos são extremamente "frágeis" e são refatorados hostis. E usar o nome também pressupõe que você esteja usando a mesma convenção de nomenclatura no Java e no banco de dados. Veja o sexo, por exemplo. Isso pode ser definido como simplesmente 'M' ou 'F' no banco de dados, mas isso não deve me impedir de usar Gender.MALE e Gender.FEMALE em Java, em vez de Gender.M ou Gender.F.
spaaarky21
2
Eu acho que um motivo pode ser que o nome e ordinal são ambos garantidos como únicos, enquanto outros valores ou campos não são. É verdade que a ordem pode mudar (não use ordinal) e o nome também pode ser alterado (não mude o nome da enumeração: P), mas também qualquer outro valor ... Não tenho certeza se vejo o grande valor em adicionar a capacidade de armazenar um valor diferente.
Svend Hansen
2
Na verdade, eu vejo o valor ... Eu excluiria essa parte do meu comentário acima, se ainda pudesse editá-lo: P: D
Svend Hansen
13
JPA2.1 terá o conversor suporte nativamente. Por favor, veja somethoughtsonjava.blogspot.fi/2013/10/…
drodil:
69

Agora isso é possível com o JPA 2.1:

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

Detalhes adicionais:

Dave Jarvis
fonte
2
O que é agora possível? Claro que podemos usar @Converter, mas enumdevemos ser manuseados com mais elegância!
YoYo
4
"Answer" faz referência a 2 links que falam sobre o uso de AttributeConverterBUT, mas cita algum código que não faz nada do tipo e não responde ao OP.
@ DN1 vontade para melhorá-lo
tvaroh
1
É a sua "resposta", então você deve "melhorá-la". Já existe uma resposta paraAttributeConverter
1
Perfeitamente trabalhando após adicionar:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28
23

No JPA 2.1, você pode usar o AttributeConverter .

Crie uma classe enumerada da seguinte forma:

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 crie um conversor 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);
    }
}

Na entidade, você só precisa de:

@Column(name = "node_type_code")

Você @Converter(autoApply = true)pode variar de acordo com o recipiente, mas testado para funcionar no Wildfly 8.1.0. Se não funcionar, você pode adicionar @Convert(converter = NodeTypeConverter.class)a coluna da classe de entidade.

Piscina
fonte
"values ​​()" deve ser "NodeType.values ​​()"
Curtis Yallop
17

A melhor abordagem seria mapear um ID exclusivo para cada tipo de enumeração, evitando as armadilhas de ORDINAL e STRING. Veja este post que descreve 5 maneiras de mapear uma enumeração.

Retirado do link acima:

1 e 2. Usando @Enumerated

Atualmente, existem duas maneiras de mapear enumerações em suas entidades JPA usando a anotação @Enumerated. Infelizmente, EnumType.STRING e EnumType.ORDINAL têm suas limitações.

Se você usar EnumType.String, renomear um de seus tipos de enum fará com que seu valor de enum fique fora de sincronia com os valores salvos no banco de dados. Se você usar EnumType.ORDINAL, excluir ou reordenar os tipos dentro de sua enum fará com que os valores salvos no banco de dados sejam mapeados para os tipos de enumeração incorretos.

Ambas as opções são frágeis. Se a enumeração for modificada sem executar uma migração de banco de dados, você poderá prejudicar a integridade de seus dados.

3. Retornos de chamada do ciclo de vida

Uma solução possível seria usar as anotações de retorno de chamada do ciclo de vida JPA, @PrePersist e @PostLoad. Isso é muito feio, pois agora você terá duas variáveis ​​em sua entidade. Um mapeando o valor armazenado no banco de dados e o outro, a enumeração real.

4. Mapeando o ID Exclusivo para cada Tipo de Enumeração

A solução preferida é mapear sua enumeração para um valor fixo, ou ID, definido na enumeração. O mapeamento para um valor fixo predefinido torna seu código mais robusto. Qualquer modificação na ordem dos tipos de enumerações ou a refatoração dos nomes não causará efeitos adversos.

5. Usando o Java EE7 @Convert

Se você estiver usando o JPA 2.1, terá a opção de usar a nova anotação @Convert. Isso requer a criação de uma classe de conversor, anotada com @Converter, dentro da qual você definiria quais valores são salvos no banco de dados para cada tipo de enumeração. Dentro da sua entidade, você anotaria sua enumeração com @Convert.

Minha preferência: (Número 4)

A razão pela qual prefiro definir meus IDs dentro da enumeração, em vez de usar um conversor, é um bom encapsulamento. Somente o tipo de enumeração deve saber seu ID e somente a entidade deve saber sobre como mapeia a enumeração para o banco de dados.

Veja a postagem original para o exemplo de código.

Chris Ritchie
fonte
1
javax.persistence.Converter é simples e com @Converter (Aplicação Automática = true) que permite manter as classes de domínio livre de anotações @Convert
Rostislav Matl
9

O problema é que eu acho que o JPA nunca foi aceito com a idéia de que já poderíamos ter um esquema complexo preexistente.

Eu acho que existem duas principais falhas resultantes disso, específicas para o Enum:

  1. A limitação do uso de nome () e ordinal (). Por que não marcar um getter com @Id, como fazemos com @Entity?
  2. Os Enum geralmente têm representação no banco de dados para permitir associação com todos os tipos de metadados, incluindo um nome próprio, um nome descritivo, talvez algo com localização etc. Precisamos do fácil uso de um Enum combinado com a flexibilidade de uma Entidade.

Ajude minha causa e vote no JPA_SPEC-47

Isso não seria mais elegante do que usar um @Converter para resolver o 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.
fonte
4

Possivelmente código relacionado próximo 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
fonte
3

Eu faria o seguinte:

Declare separadamente a enum, em seu próprio arquivo:

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 uma nova entidade JPA denominada 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);
    }


}

Você também precisará de um conversor para receber esses valores (apenas o JPA 2.1 e não há problema que eu não discuta aqui com essas enums a serem persistidas diretamente usando o conversor, portanto, será apenas uma via de mão única)

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

}

A entidade Autoridade:

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

}

Desta forma, você não pode definir diretamente para o campo enum. No entanto, você pode definir o campo Direito na autoridade usando

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

E se você precisar comparar, você pode usar:

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

O que é bem legal, eu acho. Não está totalmente correto, já que o conversor não se destina a ser usado com enum. Na verdade, a documentação diz para nunca usá-lo para esse fim; você deve usar a anotação @Enumerated. O problema é que existem apenas dois tipos de enumeração: ORDINAL ou STRING, mas o ORDINAL é complicado e não é seguro.


No entanto, se isso não lhe agradar, você pode fazer algo um pouco mais hacky e mais simples (ou não).

Vamos ver.

O 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 a entidade Autoridade

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

}

Nesta segunda idéia, não é uma situação perfeita, pois invadimos o atributo ordinal, mas é uma codificação muito menor.

Penso que a especificação JPA deve incluir o EnumType.ID onde o campo de valor de enum deve ser anotado com algum tipo de anotação @EnumId.

Carlos Cariello
fonte
2

Minha própria solução para resolver esse tipo de mapeamento Enum JPA é a seguinte.

Etapa 1 - Escreva a seguinte interface que usaremos para todas as enumerações que queremos mapear para uma coluna db:

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

    T getDbVal();

}

Etapa 2 - Implemente um conversor JPA genérico personalizado da seguinte maneira:

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

}

Essa classe converterá o valor de enum Eem um campo de banco de dados do tipo T(por exemplo String) usando getDbVal()on enum Ee vice-versa.

Etapa 3 - Deixe o enum original implementar a interface que definimos na etapa 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;
    }
}

Etapa 4 - Estenda o conversor da etapa 2 para o Rightenum da etapa 3:

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

Etapa 5 - A etapa final é anotar o campo na entidade da seguinte maneira:

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

Conclusão

IMHO, esta é a solução mais limpa e elegante se você tiver muitas enumerações para mapear e desejar usar um campo específico da enumeração em si como valor de mapeamento.

Para todas as outras enumerações no seu projeto que precisam de lógica de mapeamento semelhante, você só precisa repetir as etapas de 3 a 5, ou seja:

  • implemente a interface IDbValueno seu enum;
  • estenda o EnumDbValueConvertercódigo com apenas três linhas de código (você também pode fazer isso na sua entidade para evitar a criação de uma classe separada);
  • anote o atributo enum com @Convertfrom javax.persistencepackage.

Espero que isto ajude.

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

primeiro 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
fonte