Kartenaufzählung in JPA mit festen Werten?


192

Ich suche nach verschiedenen Möglichkeiten, eine Enumeration mit JPA abzubilden. Ich möchte insbesondere den Integer-Wert jedes Enum-Eintrags festlegen und nur den Integer-Wert speichern.

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

Eine einfache Lösung besteht darin, die Annotation Enumerated mit EnumType.ORDINAL zu verwenden:

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

In diesem Fall bildet JPA jedoch den Enum-Index (0,1,2) und nicht den gewünschten Wert (100.200.300) ab.

Die beiden Lösungen, die ich gefunden habe, scheinen nicht einfach zu sein ...

Erste Lösung

Eine hier vorgeschlagene Lösung verwendet @PrePersist und @PostLoad, um die Aufzählung in ein anderes Feld zu konvertieren und das Aufzählungsfeld als vorübergehend zu markieren:

@Basic
private int intValueForAnEnum;

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

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

Zweite Lösung

Die zweite hier vorgeschlagene Lösung schlug ein generisches Konvertierungsobjekt vor, scheint jedoch immer noch schwer und auf den Ruhezustand ausgerichtet zu sein (@Type scheint in Java EE nicht zu existieren):

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

Gibt es noch andere Lösungen?

Ich habe mehrere Ideen, aber ich weiß nicht, ob sie in JPA existieren:

  • Verwenden Sie beim Laden und Speichern des Authority-Objekts die Setter- und Getter-Methoden des rechten Mitglieds der Authority-Klasse
  • Eine äquivalente Idee wäre, JPA mitzuteilen, welche Methoden Right Enum verwendet, um Enum in Int und Int in Enum umzuwandeln
  • Gibt es eine Möglichkeit, JPA anzuweisen, einen bestimmten Konverter (RightEditor) zu verwenden, da ich Spring verwende?

7
Es ist seltsam, ORDINAL zu verwenden. Manchmal ändert jemand die Position der Elemente in der Aufzählung und die Datenbank wird zur Katastrophe
Natasha KP

2
würde das nicht auch für die Verwendung von Name gelten - jemand könnte die Namen der Aufzählung (en) ändern und sie sind wieder nicht mit der Datenbank synchronisiert ...
topchef

2
Ich stimme @NatashaKP zu. Verwenden Sie keine Ordnungszahl. Um den Namen zu ändern, gibt es so etwas nicht. Sie löschen tatsächlich die alte Aufzählung und fügen eine neue mit einem neuen Namen hinzu. Ja, alle gespeicherten Daten sind nicht synchron (Semantik, vielleicht: P).
Svend Hansen

Ja, ich kenne 5 Lösungen. Siehe meine Antwort unten, wo ich eine detaillierte Antwort habe.
Chris Ritchie

Antworten:


168

Für Versionen vor JPA 2.1 bietet JPA nur zwei Möglichkeiten, um mit Aufzählungen umzugehen, entweder nach ihren nameoder nach ihren ordinal. Und die Standard-JPA unterstützt keine benutzerdefinierten Typen. So:

  • Wenn Sie benutzerdefinierte Typkonvertierungen durchführen möchten, müssen Sie eine Provider-Erweiterung verwenden (mit Hibernate UserType, EclipseLink Converterusw.). (die zweite Lösung). ~ oder ~
  • Sie müssen den Trick @PrePersist und @PostLoad (die erste Lösung) verwenden. ~ oder ~
  • Kommentieren Sie Getter und Setter mit dem intWert ~ oder ~ und geben Sie ihn zurück
  • Verwenden Sie ein Ganzzahlattribut auf Entitätsebene und führen Sie eine Übersetzung in Getter und Setter durch.

Ich werde die neueste Option veranschaulichen (dies ist eine grundlegende Implementierung, optimieren Sie sie nach Bedarf):

@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
So traurig, dass JPA keine native Unterstützung dafür hat
Jaime Hablutzel

20
@jaime Einverstanden! Ist es verrückt zu glauben, dass Entwickler eine Aufzählung als Wert eines ihrer Felder / Eigenschaften anstelle ihres int-Werts oder Namens beibehalten möchten? Beide sind extrem "zerbrechlich" und refactoring-unfreundlich. Bei Verwendung des Namens wird außerdem davon ausgegangen, dass Sie sowohl in Java als auch in der Datenbank dieselbe Namenskonvention verwenden. Nehmen wir zum Beispiel das Geschlecht. Das könnte in der Datenbank einfach als 'M' oder 'F' definiert sein, aber das sollte mich nicht daran hindern, Gender.MALE und Gender.FEMALE in Java anstelle von Gender.M oder Gender.F zu verwenden.
spaaarky21

2
Ich denke, ein Grund könnte sein, dass sowohl der Name als auch die Ordnungszahl garantiert eindeutig sind, während andere Werte oder Felder dies nicht tun. Es ist wahr, dass sich die Reihenfolge ändern könnte (keine Ordnungszahl verwenden) und der Name auch geändert werden könnte (keine Aufzählungsnamen ändern: P), aber auch jeder andere Wert ... Ich bin nicht sicher, ob ich das sehe der große Wert mit der Möglichkeit, einen anderen Wert zu speichern.
Svend Hansen

2
Eigentlich sehe ich den Wert ... Ich würde diesen Teil meines obigen Kommentars löschen, wenn ich ihn noch bearbeiten könnte: P: D
Svend Hansen

13
JPA2.1 wird den Konverter nativ unterstützen. Bitte sehen Sie somethoughtsonjava.blogspot.fi/2013/10/…
drodil

69

Dies ist jetzt mit JPA 2.1 möglich:

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

Weitere Details:


2
Was ist jetzt möglich? Sicher können wir verwenden @Converter, enumsollten aber sofort eleganter gehandhabt werden!
YoYo

4
"Antwort" verweist auf 2 Links, die von der Verwendung eines AttributeConverterABER sprechen, zitiert einen Code, der nichts dergleichen tut und das OP nicht beantwortet.

@ DN1 fühlen Sie sich frei, es zu verbessern
Tvaroh

1
Es ist deine "Antwort", also solltest du sie "verbessern". Es gibt bereits eine Antwort fürAttributeConverter

1
Perfekt nach dem Hinzufügen arbeiten:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28

22

Ab JPA 2.1 können Sie AttributeConverter verwenden .

Erstellen Sie eine Aufzählungsklasse wie folgt:

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

Und erstellen Sie einen Konverter wie folgt:

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

Auf der Entität brauchen Sie nur:

@Column(name = "node_type_code")

Ihr Glück @Converter(autoApply = true)kann je nach Container variieren, wurde jedoch getestet, um mit Wildfly 8.1.0 zu arbeiten. Wenn es nicht funktioniert, können Sie @Convert(converter = NodeTypeConverter.class)die Entitätsklassenspalte hinzufügen .


"values ​​()" sollte "NodeType.values ​​()" sein
Curtis Yallop

17

Der beste Ansatz wäre, jedem Aufzählungstyp eine eindeutige ID zuzuordnen, um die Fallstricke von ORDINAL und STRING zu vermeiden. Siehe diesen Beitrag 5 Möglichkeiten beschrieben, wie Sie eine Aufzählung zuordnen können.

Entnommen aus dem obigen Link:

1 & 2. Verwenden von @Enumerated

Derzeit gibt es zwei Möglichkeiten, wie Sie mithilfe der Annotation @Enumerated Aufzählungen in Ihren JPA-Entitäten zuordnen können. Leider haben sowohl EnumType.STRING als auch EnumType.ORDINAL ihre Grenzen.

Wenn Sie EnumType.String verwenden, führt das Umbenennen eines Ihrer Aufzählungstypen dazu, dass Ihr Aufzählungswert nicht mit den in der Datenbank gespeicherten Werten synchronisiert ist. Wenn Sie EnumType.ORDINAL verwenden, werden beim Löschen oder Neuordnen der Typen in Ihrer Aufzählung die in der Datenbank gespeicherten Werte den falschen Aufzählungstypen zugeordnet.

Beide Optionen sind fragil. Wenn die Aufzählung geändert wird, ohne eine Datenbankmigration durchzuführen, können Sie die Integrität Ihrer Daten gefährden.

3. Lebenszyklus-Rückrufe

Eine mögliche Lösung wäre die Verwendung der Rückrufanmerkungen für den JPA-Lebenszyklus, @PrePersist und @PostLoad. Dies fühlt sich ziemlich hässlich an, da Sie jetzt zwei Variablen in Ihrer Entität haben. Eine Zuordnung des in der Datenbank gespeicherten Werts und die andere der tatsächlichen Aufzählung.

4. Ordnen Sie jedem Aufzählungstyp eine eindeutige ID zu

Die bevorzugte Lösung besteht darin, Ihre Aufzählung einem festen Wert oder einer festen ID zuzuordnen, die in der Aufzählung definiert ist. Durch die Zuordnung zu einem vordefinierten festen Wert wird Ihr Code robuster. Änderungen an der Reihenfolge der Aufzählungstypen oder die Umgestaltung der Namen haben keine nachteiligen Auswirkungen.

5. Verwenden von Java EE7 @Convert

Wenn Sie JPA 2.1 verwenden, haben Sie die Möglichkeit, die neue Annotation @Convert zu verwenden. Dies erfordert die Erstellung einer mit @Converter kommentierten Konverterklasse, in der Sie definieren, welche Werte für jeden Aufzählungstyp in der Datenbank gespeichert werden. Innerhalb Ihrer Entität würden Sie dann Ihre Aufzählung mit @Convert kommentieren.

Meine Präferenz: (Nummer 4)

Der Grund, warum ich es vorziehe, meine IDs innerhalb der Aufzählung zu definieren, anstatt einen Konverter zu verwenden, ist eine gute Kapselung. Nur der Aufzählungstyp sollte seine ID kennen, und nur die Entität sollte wissen, wie sie die Aufzählung der Datenbank zuordnet.

Die Original - Beitrag für das Codebeispiel.


1
javax.persistence.Converter ist einfach und mit @Converter (autoApply = true) können Domänenklassen frei von @ Convert-Annotationen gehalten werden
Rostislav Matl

9

Das Problem ist, denke ich, dass JPA nie mit der Idee ins Leben gerufen wurde, dass wir bereits ein komplexes, bereits existierendes Schema haben könnten.

Ich denke, dass sich daraus zwei Hauptmängel ergeben, die spezifisch für Enum sind:

  1. Die Einschränkung der Verwendung von name () und ordinal (). Warum nicht einfach einen Getter mit @Id markieren, so wie wir es mit @Entity machen?
  2. Aufzählungen sind normalerweise in der Datenbank dargestellt, um die Zuordnung zu allen Arten von Metadaten zu ermöglichen, einschließlich eines Eigennamens, eines beschreibenden Namens, möglicherweise etwas mit Lokalisierung usw. Wir benötigen die einfache Verwendung einer Aufzählung in Kombination mit der Flexibilität einer Entität.

Helfen Sie meiner Sache und stimmen Sie über JPA_SPEC-47 ab

Wäre dies nicht eleganter als die Verwendung eines @Converter zur Lösung des Problems?

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

Möglicherweise nahe verwandter Code von 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();
    }

}

2

Ich würde folgendes tun:

Deklarieren Sie die Aufzählung separat in der eigenen Datei:

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


}

Deklarieren Sie eine neue JPA-Entität mit dem Namen 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);
    }


}

Sie benötigen auch einen Konverter, um diese Werte zu erhalten (nur JPA 2.1 und es gibt ein Problem, das ich hier nicht mit diesen Aufzählungen diskutieren werde, um direkt mit dem Konverter fortzufahren, es wird also nur eine Einbahnstraße sein).

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

}

Die Behörde:

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

}

Auf diese Weise können Sie nicht direkt auf das Aufzählungsfeld setzen. Sie können jedoch das Feld Rechts in Autorität mit festlegen

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

Und wenn Sie vergleichen müssen, können Sie verwenden:

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

Was ziemlich cool ist, finde ich. Es ist nicht ganz richtig, da der Konverter nicht für die Verwendung mit Aufzählungen vorgesehen ist. In der Dokumentation heißt es, dass Sie die Annotation @Enumerated stattdessen verwenden sollten, um sie niemals für diesen Zweck zu verwenden. Das Problem ist, dass es nur zwei Aufzählungstypen gibt: ORDINAL oder STRING, aber das ORDINAL ist schwierig und nicht sicher.


Wenn es Sie jedoch nicht zufriedenstellt, können Sie etwas Hackigeres und Einfacheres tun (oder auch nicht).

Mal schauen.

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


}

und die Behörde

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

}

In dieser zweiten Idee ist es keine perfekte Situation, da wir das Ordnungsattribut hacken, aber es ist eine viel kleinere Codierung.

Ich denke, dass die JPA-Spezifikation die EnumType.ID enthalten sollte, in der das Enum-Wertfeld mit einer Art @ EnumId-Annotation versehen werden sollte.


2

Meine eigene Lösung zur Lösung dieser Art von Enum JPA-Zuordnung ist die folgende.

Schritt 1 - Schreiben Sie die folgende Schnittstelle, die wir für alle Aufzählungen verwenden, die wir einer Datenbankspalte zuordnen möchten:

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

    T getDbVal();

}

Schritt 2 - Implementieren Sie einen benutzerdefinierten generischen JPA-Konverter wie folgt:

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

}

Diese Klasse konvertiert den Aufzählungswert unter Verwendung der Aufzählung en Ein ein Datenbankfeld vom Typ T(z. B. String) und umgekehrt.getDbVal()E

Schritt 3 - Lassen Sie die ursprüngliche Aufzählung die in Schritt 1 definierte Schnittstelle implementieren:

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

Schritt 4 - Erweitern Sie den Konverter von Schritt 2 für die RightAufzählung von Schritt 3:

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

Schritt 5 - Der letzte Schritt besteht darin, das Feld in der Entität wie folgt zu kommentieren:

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

Fazit

IMHO ist dies die sauberste und eleganteste Lösung, wenn Sie viele Aufzählungen zuordnen müssen und ein bestimmtes Feld der Aufzählung selbst als Zuordnungswert verwenden möchten.

Für alle anderen Enums in Ihrem Projekt, die eine ähnliche Zuordnungslogik benötigen, müssen Sie nur die Schritte 3 bis 5 wiederholen, dh:

  • Implementieren Sie die Schnittstelle IDbValuein Ihrer Enumeration.
  • Erweitere EnumDbValueConverter mit nur 3 Codezeilen (Sie können dies auch in Ihrer Entität tun, um das Erstellen einer getrennten Klasse zu vermeiden).
  • Kommentieren Sie das enum-Attribut mit @Convertfrom javax.persistencepackage.

Hoffe das hilft.


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

erster Fall ( EnumType.ORDINAL)

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

zweiter Fall ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.