Перечисление карты в JPA с фиксированными значениями?

Я ищу различные способы сопоставления перечисления с помощью JPA. Я особенно хочу установить целочисленное значение каждой записи перечисления и сохранить только целочисленное значение.

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

Простым решением является использование Enumerated аннотации с EnumType.ORDINAL:

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

Но в этом случае JPA отображает индекс перечисления (0,1,2), а не значение, которое я хочу (100 200 300).

Два найденных вами решения не кажутся простыми...

Первое решение

Решение предлагаемое здесь, использует @PrePersist и @PostLoad для преобразования перечисления в другое поле и помечает поле перечисления как переходное:

@Basic
private int intValueForAnEnum;

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

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

Второе решение

Второе решение предлагаемое здесь, предложило общий объект преобразования, но все еще кажется тяжелым и спящим-ориентированным (@Type, похоже, не существует в 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")
            }
)

Есть ли другие решения?

У меня есть несколько идей, но я не знаю, существуют ли они в JPA:

  • используйте методы setter и getter правого члена класса полномочий при загрузке и сохранении объекта Authority.
  • эквивалентная идея заключалась бы в том, чтобы сообщить JPA, каковы методы Right enum для преобразования enum в int и int для перечисления
  • Поскольку я использую Spring, есть ли способ сообщить JPA использовать определенный конвертер (RightEditor)?

Ответ 1

Для версий, предшествующих JPA 2.1, JPA предоставляет только два способа обработки перечислений, их name или их ordinal. И стандартная JPA не поддерживает пользовательские типы. Итак:

  • Если вы хотите сделать преобразование нестандартного типа, вам нужно будет использовать расширение поставщика (с Hibernate UserType, EclipseLink Converter и т.д.). (второе решение). ~ Или ~
  • Вам придется использовать трюк @PrePersist и @PostLoad (первое решение). ~ Или ~
  • Аннотировать приемник и наборщик и вернуть значение int ~ или ~
  • Используйте целочисленный атрибут на уровне сущности и выполняйте перевод в геттерах и сеттерах.

Я проиллюстрирую последний вариант (это базовая реализация, настройка по мере необходимости):

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

}

Ответ 3

Лучшим подходом было бы сопоставление уникального идентификатора каждому типу перечисления, что позволило бы избежать ошибок ORDINAL и STRING. См. Это post, в котором перечислены 5 способов отображения перечисления.

Взято по ссылке выше:

1 & 2. Использование @Enumerated

В настоящее время существует 2 способа сопоставления перечислений в ваших судах JPA с помощью @Enumerated аннотации. К сожалению, у EnumType.STRING и EnumType.ORDINAL есть свои ограничения.

Если вы используете EnumType.String, то переименование одного из ваших типов перечислений приведет к тому, что значение вашего перечисления будет не синхронизировано со значениями, сохраненными в базе данных. Если вы используете EnumType.ORDINAL, то удаление или переупорядочение типов в вашем перечислении приведет к тому, что значения, сохраненные в базе данных, будут отображаться с неправильными типами перечислений.

Оба эти параметра являются хрупкими. Если перечисление изменено без миграции базы данных, вы можете подвергнуть опасности целостность ваших данных.

3. Обратные вызовы жизненного цикла

Возможное решение было бы использовать аннотации обратного вызова цикла JPA, @PrePersist и @PostLoad. Это кажется довольно уродливым, так как теперь у вас будет две переменные в вашей организации. Один сопоставляет значение, хранящееся в базе данных, а другое - фактическое перечисление.

4. Отображение уникального идентификатора для каждого типа перечисления

Предпочтительным решением является сопоставление вашего перечисления с фиксированным значением или идентификатором, определенным в перечислении. Сопоставление с предопределенным фиксированным значением делает ваш код более надежным. Любая модификация порядка типов перечислений или рефакторинг имен не приведет к каким-либо неблагоприятным последствиям.

5. Использование Java EE7 @Convert

Если вы используете JPA 2.1, у вас есть возможность использовать новую аннотацию @Convert. Для этого требуется создание класса преобразователя, аннотированного с помощью @Converter, внутри которого вы определяете, какие значения сохраняются в базе данных для каждого типа перечисления. Внутри вашей сущности вы затем аннотировали свое перечисление с помощью @Convert.

Мои предпочтения: (Номер 4)

Причина, по которой я предпочитаю определять свой идентификатор в enum как против использования конвертера, является хорошей инкапсуляцией. Только тип перечисления должен знать свой идентификатор, и только сущность должна знать, как он отображает перечисление в базу данных.

Для примера кода см. оригинал post.

Ответ 4

Из JPA 2.1 вы можете использовать AttributeConverter.

Создайте перечислимый класс следующим образом:

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

И создайте конвертер вот так:

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

На сущности вам просто нужно:

@Column(name = "node_type_code")

Удача с @Converter(autoApply = true) может варьироваться в зависимости от контейнера, но протестирована на работу с Wildfly 8.1.0. Если это не работает, вы можете добавить @Convert(converter = NodeTypeConverter.class) в столбец класса сущности.

Ответ 5

Проблема в том, что, по-моему, JPA никогда не предполагала, что у нас может быть уже существовавшая ранее схема.

Я думаю, что из-за этого возникают два основных недостатка, характерных для Enum:

  • Ограничение использования name() и порядкового(). Почему бы просто не отметить геттер с помощью @Id, как мы это делаем с @Entity?
  • Enum обычно имеет представление в базе данных, чтобы разрешить ассоциацию со всеми видами метаданных, включая собственное имя, описательное имя, возможно, что-то с локализацией и т.д. Нам нужно просто использовать Enum в сочетании с гибкостью Entity.

Помогите моему делу и проголосуйте JPA_SPEC-47

Не будет ли это более элегантным, чем использование @Converter для решения проблемы?

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

Ответ 6

Возможно, близкий связанный код 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 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();
    }

}

Ответ 7

Я бы сделал следующее:

Объявите отдельно перечисление в своем собственном файле:

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


}

Объявить новый объект JPA с именем 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);
    }


}

Вам также понадобится конвертер для получения этих значений (только JPA 2.1, и есть проблема, которую я не буду здесь обсуждать с этими перечислениями, которые будут напрямую сохраняться с помощью конвертера, так что это будет только односторонняя дорога)

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

}

Орган управления:

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

}

Поступая таким образом, вы не можете установить непосредственно поле enum. Однако вы можете установить поле Right в Authority, используя

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

И если вам нужно сравнить, вы можете использовать:

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

Что довольно круто, я думаю. Это не совсем правильно, так как конвертер не предназначен для использования с enum. На самом деле, в документации сказано, что никогда не используйте его для этой цели, вместо этого вы должны использовать аннотацию @Enumerated. Проблема в том, что существует только два типа enum: ORDINAL или STRING, но ORDINAL сложен и небезопасен.


Однако, если это не удовлетворяет вас, вы можете сделать что-то более хакерское и более простое (или нет).

Посмотрим.

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


}

и орган власти

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

}

В этой второй идее, это не идеальная ситуация, так как мы взломали порядковый атрибут, но это гораздо меньшая кодировка.

Я думаю, что спецификация JPA должна включать EnumType.ID, где поле значения enum должно быть аннотировано с помощью своего рода аннотации @EnumId.