Wyliczanie mapy w JPA ze stałymi wartościami?


192

Szukam różnych sposobów mapowania wyliczenia za pomocą JPA. W szczególności chcę ustawić wartość całkowitą każdego wpisu wyliczenia i zapisać tylko wartość całkowitą.

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

Prostym rozwiązaniem jest użycie wyliczonej adnotacji w EnumType.ORDINAL:

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

Ale w tym przypadku JPA odwzorowuje indeks wyliczeniowy (0,1,2), a nie żądaną wartość (100,200,300).

Dwa znalezione przeze mnie rozwiązania nie wydają się proste ...

Pierwsze rozwiązanie

Proponowane tutaj rozwiązanie wykorzystuje @PrePersist i @PostLoad do konwersji wyliczenia na inne pole i oznaczenia pola wyliczenia jako przejściowe:

@Basic
private int intValueForAnEnum;

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

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

Drugie rozwiązanie

Drugie zaproponowane tutaj rozwiązanie zaproponowało ogólny obiekt konwersji, ale nadal wydaje się ciężki i zorientowany na hibernację (@Type wydaje się nie istnieć w 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")
            }
)

Czy są jakieś inne rozwiązania?

Mam na myśli kilka pomysłów, ale nie wiem, czy istnieją one w JPA:

  • używaj metody ustawiającej i pobierającej prawego członka klasy klasy Authority podczas ładowania i zapisywania obiektu klasy Authority
  • równoważnym pomysłem byłoby poinformowanie JPA, jakie są metody Right enum do konwersji enum na int i int na enum
  • Ponieważ używam Springa, czy jest jakiś sposób, aby powiedzieć JPA, aby używał określonego konwertera (RightEditor)?

7
Dziwnie jest używać ORDINALNEGO, że ktoś czasami zmienia miejsce elementów w wyliczeniu, a baza danych staje się katastrofą
Natasha KP

2
nie to samo dotyczy korzystania z nazwy - ktoś może zmienić nazwy enum i znów nie jest zsynchronizowany z bazą danych ...
topchef

2
Zgadzam się z @NatashaKP. Nie używaj porządkowej. Aby zmienić nazwę, nie ma czegoś takiego. W rzeczywistości usuwasz stary wyliczenie i dodajesz nowy z nową nazwą, więc tak, wszystkie przechowywane dane nie byłyby zsynchronizowane (semantyka, być może: P).
Svend Hansen

Tak, istnieje 5 rozwiązań, które znam. Zobacz moją odpowiedź poniżej, gdzie mam szczegółową odpowiedź.
Chris Ritchie

Odpowiedzi:


168

W przypadku wersji wcześniejszych niż JPA 2.1, JPA zapewnia tylko dwa sposoby radzenia sobie z wyliczeniami, przez ich namelub przez ich ordinal. Standardowy JPA nie obsługuje niestandardowych typów. Więc:

  • Jeśli chcesz wykonać konwersję typu niestandardowego, musisz użyć rozszerzenia dostawcy (z Hibernacją UserType, EclipseLink Converteritp.). (drugie rozwiązanie). ~ lub ~
  • Będziesz musiał użyć sztuczki @PrePersist i @PostLoad (pierwsze rozwiązanie). ~ lub ~
  • Opisz getterowi i seterowi pobieranie i zwracanie intwartości ~ lub ~
  • Użyj atrybutu liczby całkowitej na poziomie encji i przeprowadź tłumaczenie w modułach pobierających i ustawiających.

Zilustruję najnowszą opcję (jest to podstawowa implementacja, dostosuj ją w razie potrzeby):

@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
Tak smutne, że JPA nie ma na to natywnego wsparcia
Jaime Hablutzel,

20
@jaime uzgodnione! Czy to szalone, że programiści mogą chcieć zachować wyliczenie jako wartość jednego z jego pól / właściwości zamiast wartości int lub nazwy? Oba są wyjątkowo „kruche” i nieprzyjazne dla refaktoryzacji. Używanie nazwy zakłada również, że używasz tej samej konwencji nazewnictwa zarówno w Javie, jak i bazie danych. Weźmy na przykład płeć. Można to zdefiniować jako po prostu „M” lub „F” w bazie danych, ale nie powinno to uniemożliwiać mi używania Gender.MALE i Gender.FEMALE w Javie, zamiast Gender.M lub Gender.F.
spaaarky21

2
Myślę, że powodem może być to, że zarówno nazwa, jak i porządek są gwarantowane, aby były unikalne, podczas gdy inne wartości lub pola nie są. To prawda, że ​​kolejność może się zmienić (nie używaj porządkowej), a nazwa może być również zmieniona (nie zmieniaj nazw enum: P), ale może to zrobić każda inna wartość ... Nie jestem pewien, czy widzę wielka wartość z dodaniem możliwości przechowywania innej wartości.
Svend Hansen

2
Właściwie to widzę wartość ... Usunąłbym tę część mojego komentarza powyżej, gdybym mógł ją jeszcze edytować: P: D
Svend Hansen

13
JPA2.1 będzie miał wbudowaną obsługę konwertera. Proszę zapoznaj się z somethoughtsonjava.blogspot.fi/2013/10/…
drodil

69

Jest to teraz możliwe w JPA 2.1:

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

Dalsze szczegóły:


2
Co jest teraz możliwe? Jasne, że możemy użyć @Converter, ale enumnależy postępować bardziej elegancko po wyjęciu z pudełka!
YoYo

4
„Odpowiedź” odnosi się do 2 linków, które mówią o użyciu AttributeConverterALE cytuje kod, który nic nie robi i nie odpowiada OP.

@ DN1 możesz go ulepszyć
Tvaroh,

1
To twoja „odpowiedź”, więc powinieneś ją „ulepszyć”. Odpowiedź już istniejeAttributeConverter

1
Doskonale działa po dodaniu:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28

23

Od JPA 2.1 możesz używać AttributeConverter .

Utwórz wyliczoną klasę w następujący sposób:

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

I utwórz konwerter taki jak ten:

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 encji potrzebujesz:

@Column(name = "node_type_code")

Twoje szczęście @Converter(autoApply = true)może się różnić w zależności od kontenera, ale przetestowane pod kątem działania na Wildfly 8.1.0. Jeśli to nie działa, możesz dodać @Convert(converter = NodeTypeConverter.class)kolumnę klasy encji.


„wartości ()” powinny być „NodeType.values ​​()”
Curtis Yallop

17

Najlepszym rozwiązaniem byłoby odwzorowanie unikalnego identyfikatora na każdy typ wyliczenia, unikając w ten sposób pułapek ORDINAL i STRING. Zobacz ten post, który opisuje 5 sposobów mapowania wyliczenia.

Z linku powyżej:

1 i 2. Korzystanie z @Enumerated

Obecnie istnieją 2 sposoby mapowania wyliczeń w jednostkach JPA za pomocą adnotacji @Enumerated. Niestety zarówno EnumType.STRING, jak i EnumType.ORDINAL mają swoje ograniczenia.

Jeśli użyjesz EnumType.String, wówczas zmiana nazwy jednego z typów wyliczenia spowoduje, że wartość wyliczenia nie będzie zsynchronizowana z wartościami zapisanymi w bazie danych. Jeśli użyjesz EnumType.ORDINAL, wówczas usunięcie lub zmiana kolejności typów w twoim wyliczeniu spowoduje mapowanie wartości zapisanych w bazie danych na niewłaściwe typy wyliczeń.

Obie te opcje są delikatne. Jeśli wyliczenie zostanie zmodyfikowane bez przeprowadzania migracji bazy danych, można narzucić integralność danych.

3. Callback cyklu życia

Możliwym rozwiązaniem byłoby użycie adnotacji zwrotnych JPA cyklu życia, @PrePersist i @PostLoad. Czuje się to dość brzydko, ponieważ będziesz mieć teraz dwie zmienne w swojej jednostce. Jeden odwzorowuje wartość przechowywaną w bazie danych, a drugi rzeczywisty wyliczenie.

4. Mapowanie unikalnego identyfikatora do każdego typu wyliczenia

Preferowanym rozwiązaniem jest odwzorowanie wyliczenia na stałą wartość lub identyfikator zdefiniowany w wyliczeniu. Odwzorowanie na predefiniowaną, stałą wartość sprawia, że ​​kod jest bardziej niezawodny. Jakakolwiek zmiana kolejności typów wyliczeń lub refaktoryzacja nazw nie spowoduje żadnych negatywnych skutków.

5. Korzystanie z Java EE7 @Convert

Jeśli używasz JPA 2.1, możesz skorzystać z nowej adnotacji @Convert. Wymaga to utworzenia klasy konwertera, opatrzonej adnotacją @Converter, w której zdefiniowałbyś, jakie wartości są zapisywane w bazie danych dla każdego typu wyliczenia. W swojej jednostce adnotowałbyś wtedy swój wyliczenie za pomocą @Convert.

Moje preferencje: (numer 4)

Powodem, dla którego wolę definiować moje identyfikatory w wyliczeniu zamiast używania konwertera, jest dobra enkapsulacja. Tylko typ wyliczeniowy powinien znać swój identyfikator, a tylko jednostka powinna wiedzieć, w jaki sposób mapuje wyliczenie do bazy danych.

Zobacz oryginalny post dla przykładu kodu.


1
javax.persistence.Converter jest prosty, a dzięki @Converter (autoApply = true) pozwala utrzymać klasy domen wolne od adnotacji @Convert
Rostislav Matl

9

Problemem jest, jak sądzę, to, że JPA nigdy nie wpadł na pomysł, że moglibyśmy już mieć złożony wcześniej istniejący schemat.

Myślę, że wynikają z tego dwa główne niedociągnięcia, specyficzne dla Enum:

  1. Ograniczenie używania name () i ordinal (). Dlaczego nie oznaczyć gettera za pomocą @Id, tak jak to robimy z @Entity?
  2. Enum mają zwykle reprezentację w bazie danych, aby umożliwić skojarzenie z wszelkiego rodzaju metadanymi, w tym z odpowiednią nazwą, nazwą opisową, może z lokalizacją itp. Potrzebujemy łatwego użycia Enum w połączeniu z elastycznością Entity.

Pomóż mojej sprawie i zagłosuj na JPA_SPEC-47

Czy nie byłoby to bardziej eleganckie niż użycie @Converter do rozwiązania problemu?

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

Prawdopodobnie blisko powiązany kod Pascala

@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

Zrobiłbym następujące:

Deklaruj osobno wyliczenie we własnym pliku:

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


}

Zadeklaruj nowy podmiot JPA o nazwie Prawo

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


}

Będziesz także potrzebował konwertera do odbierania tych wartości (tylko JPA 2.1 i jest problem, nie będę tutaj omawiać z tymi wyliczeniami, które będą bezpośrednio utrwalane za pomocą konwertera, więc będzie to droga tylko w jedną stronę)

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

}

Organ:

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

}

W ten sposób nie można ustawić bezpośrednio w polu wyliczenia. Można jednak ustawić odpowiednie pole w polu Uprawnienia za pomocą

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

A jeśli chcesz porównać, możesz użyć:

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

Co wydaje mi się całkiem fajne. Nie jest to całkowicie poprawne, ponieważ konwerter nie jest przeznaczony do użytku z enum. W rzeczywistości dokumentacja mówi, aby nigdy nie używać jej do tego celu, zamiast tego należy użyć adnotacji @Enumerated. Problem polega na tym, że istnieją tylko dwa typy wyliczeń: ORDINAL lub STRING, ale ORDINAL jest trudny i nie jest bezpieczny.


Jeśli jednak nie jest to satysfakcjonujące, możesz zrobić coś nieco bardziej zuchwałego i prostszego (lub nie).

Zobaczmy.

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


}

i podmiot władzy

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

}

W tym drugim pomyśle nie jest to idealna sytuacja, ponieważ włamujemy się do atrybutu porządkowego, ale jest to znacznie mniejsze kodowanie.

Myślę, że specyfikacja JPA powinna zawierać EnumType.ID, w którym pole wartości wyliczeniowej powinno być opatrzone adnotacją @EnumId.


2

Moje własne rozwiązanie do rozwiązania tego rodzaju mapowania Enum JPA jest następujące.

Krok 1 - Napisz następujący interfejs, którego będziemy używać dla wszystkich wyliczeń, które chcemy zamapować na kolumnę db:

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

    T getDbVal();

}

Krok 2 - Zaimplementuj niestandardowy ogólny konwerter JPA w następujący sposób:

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

}

Ta klasa przekonwertuje wartość wyliczenia Ena pole bazy danych typu T(np. String) Przy użyciu getDbVal()on enum Ei odwrotnie.

Krok 3 - Pozwól oryginalnemu enum zaimplementować interfejs zdefiniowany w kroku 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;
    }
}

Krok 4 - Rozszerz konwerter z kroku 2 o Rightwyliczenie z kroku 3:

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

Krok 5 - Ostatnim krokiem jest opisanie pola w encji w następujący sposób:

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

Wniosek

IMHO jest to najczystsze i najbardziej eleganckie rozwiązanie, jeśli masz wiele wyliczeń do odwzorowania i chcesz użyć określonego pola wyliczenia jako wartości odwzorowania.

W przypadku wszystkich innych wyliczeń w projekcie, które wymagają podobnej logiki odwzorowania, wystarczy powtórzyć kroki od 3 do 5, to znaczy:

  • zaimplementuj interfejs IDbValuena swoim enum;
  • rozszerz EnumDbValueConvertertylko 3 linie kodu (możesz to zrobić również w swojej jednostce, aby uniknąć tworzenia oddzielnej klasy);
  • adnotuj atrybut wyliczania za pomocą @Convertz javax.persistencepakietu.

Mam nadzieję że to pomoże.


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

pierwszy przypadek ( EnumType.ORDINAL)

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

druga sprawa ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.