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

192

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

@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 при загрузке и сохранении объекта Authority
  • эквивалентной идеей было бы рассказать JPA, каковы методы Right enum для преобразования enum в int и int в enum
  • Поскольку я использую Spring, есть ли способ сказать JPA использовать определенный конвертер (RightEditor)?
Kartoch
источник
7
Странно использовать ORDINAL, кто-то иногда меняет местами пункты в перечне, и база данных становится катастрофой
Наташа КП
2
это не относится к использованию Name - кто-то может изменить имена перечислений, и они снова не синхронизированы с базой данных ...
topchef
2
Я согласен с @NatashaKP. Не используйте порядковый номер. Для смены имени такого нет. Вы фактически удаляете старое перечисление и добавляете новое с новым именем, так что да, любые сохраненные данные будут не синхронизированы (семантика, возможно: P).
Свен Хансен,
Да, есть 5 решений, о которых я знаю. Смотрите мой ответ ниже, где у меня есть подробный ответ.
Крис Ричи

Ответы:

168

Для версий более ранних, чем 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();
    }

}
Паскаль Тивент
источник
50
Так что печальный JPA не имеет нативной поддержки для этого
Хайме Хаблутцель
20
@jaime Согласен! Неужели думать, что разработчики захотят сохранить перечисление как значение одного из его полей / свойств вместо его значения или имени int? Оба из них чрезвычайно «хрупкие» и рефакторинг-недружественные. И использование имени также предполагает, что вы используете одно и то же соглашение об именах как в Java, так и в базе данных. Взять хотя бы пол. Это может быть определено как просто 'M' или 'F' в базе данных, но это не должно помешать мне использовать Gender.MALE и Gender.FEMALE в Java вместо Gender.M или Gender.F.
spaaarky21
2
Я предполагаю, что причина может заключаться в том, что имя и порядковый номер гарантированно будут уникальными, тогда как любые другие значения или поля - нет. Это правда, что порядок может измениться (не используйте порядковый номер), а также может быть изменено и имя (не меняйте имена перечислений: P), но может измениться и любое другое значение ... Я не уверен, что вижу большое значение с добавлением возможности хранить другое значение.
Свен Хансен,
2
На самом деле, я вижу значение ... Я бы удалил эту часть моего комментария выше, если бы я все еще мог ее отредактировать: P: D
Свенд Хансен
13
JPA2.1 будет иметь встроенную поддержку конвертера. Пожалуйста, смотрите someoughttsonjava.blogspot.fi/2013/10/…
drodil
69

Теперь это возможно с JPA 2.1:

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

Дальнейшие подробности:

Дейв Джарвис
источник
2
Что теперь возможно? Конечно, мы можем использовать @Converter, но enumдолжны быть обработаны более элегантно из коробки!
Йо-
4
«Ответить» ссылается на 2 ссылки, в которых говорится об использовании AttributeConverterНО, цитируется некоторый код, который ничего не делает и не отвечает на ОП.
@ DN1 не стесняйтесь улучшать это
Tvaroh
1
Это ваш «ответ», поэтому вы должны «улучшить его». Там уже есть ответ дляAttributeConverter
1
Отлично работает после добавления:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28
23

Из 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)столбец класса сущности.

Бассейн
источник
«values ​​()» должно быть «NodeType.values ​​()»
Кертис
17

Наилучшим подходом будет сопоставление уникального идентификатора с каждым типом перечисления, что позволит избежать ошибок ORDINAL и STRING. Посмотрите этот пост, в котором изложены 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, внутри которого вы будете определять, какие значения сохраняются в базе данных для каждого типа перечисления. Внутри вашей сущности вы можете аннотировать свой enum с помощью @Convert.

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

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

Смотрите оригинальный пост для примера кода.

Крис Ричи
источник
1
javax.persistence.Converter прост и с помощью @Converter (autoApply = true) позволяет сохранять классы доменов свободными от аннотаций @Convert
Ростислав Матл
9

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

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

  1. Ограничение использования name () и ordinal (). Почему бы просто не пометить геттер @Id, как мы делаем с @Entity?
  2. 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;
  }
}
оборота JoD.
источник
4

Возможно закрыть связанный код Паскаля

@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

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

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

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, и есть проблема, которую я не буду обсуждать здесь с этими enum'ами, которые будут напрямую сохраняться с помощью конвертера, так что это будет только односторонняя дорога)

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.

Карлос Кариелло
источник
2

Мое собственное решение для решения этого вида отображения Enum JPA заключается в следующем.

Шаг 1 - Напишите следующий интерфейс, который мы будем использовать для всех перечислений, которые мы хотим отобразить в столбец БД:

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

    T getDbVal();

}

Шаг 2 - Реализуйте пользовательский универсальный конвертер JPA следующим образом:

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

}

Этот класс преобразует значение enum Eв поле базы данных типа T(например String), используя getDbVal()on enum E, и наоборот.

Шаг 3 - Пусть оригинальный enum реализует интерфейс, который мы определили на шаге 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;
    }
}

Шаг 4 - Расширьте конвертер шага 2 для Rightперечисления шага 3:

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

Шаг 5 - Последний шаг - аннотировать поле в сущности следующим образом:

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

Вывод

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

Для всех остальных перечислений в вашем проекте, которые требуют аналогичной логики сопоставления, вам нужно только повторить шаги с 3 по 5, то есть:

  • реализовать интерфейс IDbValueв вашем перечислении;
  • расширьте его EnumDbValueConverterвсего тремя строками кода (вы также можете делать это внутри вашей сущности, чтобы избежать создания отдельного класса);
  • аннотировать атрибут перечислимого с @Convertиз javax.persistenceпакета.

Надеюсь это поможет.

Данило Чанчулли
источник
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

первый случай ( EnumType.ORDINAL)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood      0   
  2  Geoff Dalgas     0   
  3 Jarrod Jesica     1   
  4  Joel Lucy        1   
╚════╩══════════════╩════════╝

второй случай ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Махди Бен Селимене
источник