Мапа перерахунку в 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;
}

Простим рішенням є використання переліченої анотації з 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:

  • використовуйте методи встановлення та отримання для правого члена класу повноважень під час завантаження та збереження об'єкта влади
  • еквівалентною ідеєю було б сказати JPA, які методи Right enum для перетворення enum в int та int в enum
  • Оскільки я використовую Spring, чи є спосіб сказати JPA використовувати певний перетворювач (RightEditor)?

7
Дивно використовувати ORDINAL, колись хтось змінить місця елементів у перерахунку, і база даних стане катастрофою
Наташа КП

2
не буде те ж саме, що стосується використання Імені - хтось може змінити ім'я перерахунків, і вони знову не синхронізуються з базою даних ...
topchef

2
Я згоден з @NatashaKP. Не використовуйте порядкових. Для зміни імені такого немає. Ви фактично видаляєте старий enum і додаєте новий з новим ім'ям, так що так, будь-які збережені дані не синхронізуються (семантика, можливо: P).
Svend Hansen

Так, я знаю 5 рішень. Дивіться мою відповідь нижче, де я маю детальну відповідь.
Кріс Річі

Відповіді:


168

Для версій, що передують JPA 2.1, JPA надає лише два способи боротьби з перерахунками, їхніми nameабо їхніми ordinal. А стандартний JPA не підтримує власні типи. Так:

  • Якщо ви хочете зробити конверсії спеціального типу, вам доведеться скористатися розширенням постачальника послуг (з режиму UserTypehibernate Converter, EclipseLink тощо). (друге рішення). ~ або ~
  • Вам доведеться скористатися трюком @PrePersist і @PostLoad (перше рішення). ~ або ~
  • Анотувати гетерограф і сетер, приймаючи та повертаючи intзначення ~ або ~
  • Використовуйте атрибут integer на рівні сутності та виконайте переклад у getters та setters.

Я проілюструю останній варіант (це основна реалізація, налаштуйте його як потрібно):

@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), але так би могло бути і будь-яке інше значення ... Я не впевнений, що бачу велика цінність із додаванням можливості зберігати інше значення.
Svend Hansen

2
Насправді я бачу цінність ... Я б видалив цю частину свого коментаря вище, якби я все-таки зміг її відредагувати: P: D
Svend Hansen

13
JPA2.1 матиме підтримку перетворювача. Будь ласка, дивіться somethoughtsonjava.blogspot.fi/2013/10/…
drodil

69

Тепер це можливо через JPA 2.1:

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

Детальніше:


2
Що зараз можливо? Впевнені, що ми можемо використовувати @Converter, але з ними enumпотрібно вишукатись більш елегантно!
YoYo

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)в стовпець класу сутності.


"Значення ()" мають бути "NodeType.values ​​()"
Кертіс

17

Найкращим підходом було б зіставлення унікального ідентифікатора для кожного типу перерахунків, уникаючи таким чином підводних каменів ORDINAL та STRING. Дивіться цю публікацію, де викладено 5 способів зіставити перерахунок.

Взяте за посиланням вище:

1 і 2. Використовуючи @Enumerated

В даний час є два способи відображення переліків у ваших об'єктах JPA, використовуючи анотацію @Enumerated. На жаль, і EnumType.STRING, і EnumType.ORDINAL мають свої обмеження.

Якщо ви використовуєте EnumType.String, перейменування одного з типів enum призведе до того, що значення enum не синхронізується зі значеннями, збереженими в базі даних. Якщо ви використовуєте EnumType.ORDINAL, вилучення або переупорядкування типів у вашій enum призведе до того, що значення, збережені в базі даних, відображаються на неправильні типи перерахунків.

Обидва ці варіанти крихкі. Якщо перерахунок буде змінено без проведення міграції бази даних, ви можете поставити під загрозу цілісність своїх даних.

3. Відкликання життєвого циклу

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

4. Зіставлення унікального ідентифікатора для кожного типу перерахунку

Кращим рішенням є зіставлення вашого перерахунку до фіксованого значення або ідентифікатора, визначеного в enum. Зображення до заздалегідь визначеного фіксованого значення робить ваш код більш надійним. Будь-яка зміна порядку переліку типів перерахунків або перейменовування імен не спричинить негативних наслідків.

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. Обмеження використання імені () та порядкового (). Чому б просто не позначити геттера з @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;
  }
}

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 під назвою Право

@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. Однак ви можете встановити правильне поле в службі, використовуючи

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

А якщо вам потрібно порівняти, ви можете використовувати:

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

Що досить класно, я думаю. Це не зовсім коректно, оскільки перетворювач не призначений для використання з enum´s. Насправді в документації сказано, що ніколи не використовуйте її для цієї мети, замість цього слід використовувати анотацію @Enumerated. Проблема полягає в тому, що є лише два типи перерахунків: ORDINAL або STRING, але ORDINAL є складним і не безпечним.


Однак якщо це вас не влаштовує, ви можете зробити щось трохи більш хакітніше і простіше (чи ні).

Подивимось

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 - Напишіть наступний інтерфейс, який ми будемо використовувати для всіх перерахунків, які ми хочемо зіставити в колонку db:

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лише 3 рядки коду (ви також можете зробити це у вашій сутності, щоб уникнути створення окремого класу);
  • анотувати атрибут перечислимого з @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 
╚════╩══════════════╩════════╝
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.