Peta enum di JPA dengan nilai tetap?

192

Saya mencari berbagai cara untuk memetakan enum menggunakan JPA. Saya terutama ingin mengatur nilai integer dari setiap entri enum dan hanya menyimpan nilai integer.

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

Solusi sederhana adalah dengan menggunakan anotasi Enumerated dengan EnumType.ORDINAL:

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

Tetapi dalam hal ini JPA memetakan indeks enum (0,1,2) dan bukan nilai yang saya inginkan (100,200,300).

Dua solusi yang saya temukan sepertinya tidak sederhana ...

Solusi Pertama

Solusi, diusulkan di sini , menggunakan @PrePersist dan @PostLoad untuk mengonversi enum ke bidang lain dan menandai bidang enum sebagai transien:

@Basic
private int intValueForAnEnum;

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

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

Solusi Kedua

Solusi kedua yang diusulkan di sini mengusulkan objek konversi generik, tetapi masih tampak berat dan berorientasi hibernasi (@Type sepertinya tidak ada di 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")
            }
)

Apakah ada solusi lain?

Saya memiliki beberapa ide tetapi saya tidak tahu apakah mereka ada di JPA:

  • menggunakan metode setter dan pengambil dari anggota kanan dari Kelas Otoritas saat memuat dan menyimpan objek Otoritas
  • ide yang setara adalah untuk memberi tahu JPA apa metode Right enum untuk mengonversi enum ke int dan int ke enum
  • Karena saya menggunakan Spring, apakah ada cara untuk memberitahu JPA untuk menggunakan konverter tertentu (RightEditor)?
Kartoch
sumber
7
Sungguh aneh menggunakan ORDINAL seseorang kadang-kadang akan mengubah tempat item dalam pencacahan dan database akan menjadi bencana
Natasha KP
2
tidakkah hal yang sama berlaku untuk menggunakan Nama - seseorang dapat mengubah nama enum dan mereka lagi tidak sinkron dengan basis data ...
topchef
2
Saya setuju dengan @NatashaKP. Jangan gunakan ordinal. Untuk mengubah nama, tidak ada yang namanya. Anda sebenarnya menghapus enum lama dan menambahkan yang baru dengan nama baru, jadi ya, setiap data yang disimpan akan tidak sinkron (semantik, mungkin: P).
Svend Hansen
Ya ada 5 solusi yang saya tahu. Lihat jawaban saya di bawah, di mana saya memiliki jawaban terperinci.
Chris Ritchie

Jawaban:

168

Untuk versi yang lebih awal dari JPA 2.1, JPA hanya menyediakan dua cara untuk menangani enum, dengan mereka nameatau oleh mereka ordinal. Dan JPA standar tidak mendukung tipe khusus. Begitu:

  • Jika Anda ingin melakukan konversi jenis khusus, Anda harus menggunakan ekstensi penyedia (dengan Hibernate UserType, EclipseLink Converter, dll). (solusi kedua). ~ atau ~
  • Anda harus menggunakan trik @PrePersist dan @PostLoad (solusi pertama). ~ atau ~
  • Anotasi pengambil dan setter mengambil dan mengembalikan intnilai ~ atau ~
  • Gunakan atribut integer pada level entitas dan lakukan terjemahan dalam getter dan setter.

Saya akan menggambarkan opsi terbaru (ini adalah implementasi dasar, atur sesuai kebutuhan):

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

}
Thivent Pascal
sumber
50
JPA sangat sedih tidak memiliki dukungan asli untuk ini
Jaime Hablutzel
20
@jaime Setuju! Apakah gila berpikir bahwa pengembang mungkin ingin mempertahankan enum sebagai nilai salah satu bidang / propertinya alih-alih nilai int atau namanya? Keduanya sangat "rapuh" dan tidak ramah terhadap refactoring. Dan menggunakan namanya juga mengasumsikan Anda menggunakan konvensi penamaan yang sama di Jawa dan database. Ambil contoh gender. Itu mungkin didefinisikan sebagai hanya 'M' atau 'F' dalam database tetapi itu seharusnya tidak mencegah saya menggunakan Gender.MALE dan Gender.FEMALE di Jawa, bukan Gender.M atau Gender.F.
spaaarky21
2
Saya kira alasannya bisa jadi nama dan ordinal keduanya dijamin unik, sedangkan nilai atau bidang lainnya tidak. Memang benar bahwa pesanan dapat berubah (tidak menggunakan ordinal) dan namanya juga dapat diubah (jangan mengubah nama enum: P), tetapi demikian juga nilai lainnya ... Saya tidak yakin saya melihat nilai hebat dengan menambahkan kemampuan untuk menyimpan nilai yang berbeda.
Svend Hansen
2
Sebenarnya, saya melihat nilainya ... Saya akan menghapus bagian dari komentar saya di atas, jika saya masih dapat mengeditnya: P: D
Svend Hansen
13
JPA2.1 akan memiliki dukungan konverter secara asli. Silakan lihat somethoughtsonjava.blogspot.fi/2013/10/…
drodil
69

Ini sekarang dimungkinkan dengan JPA 2.1:

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

Keterangan lebih lanjut:

Dave Jarvis
sumber
2
Apa yang sekarang mungkin? Tentu kita bisa menggunakan @Converter, tetapi enumharus ditangani dengan lebih elegan di luar kotak!
YoYo
4
"Jawab" referensi 2 tautan yang berbicara tentang menggunakan AttributeConverterTETAPI mengutip beberapa kode yang tidak melakukan hal semacam itu dan tidak menjawab OP.
@ DN1 merasa bebas untuk memperbaikinya
Tvaroh
1
Itu adalah "jawaban" Anda, jadi Anda harus "memperbaikinya". Sudah ada jawaban untukAttributeConverter
1
Sempurna bekerja setelah menambahkan:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28
23

Dari JPA 2.1 Anda dapat menggunakan AttributeConverter .

Buat kelas yang disebutkan seperti ini:

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

Dan buat konverter seperti ini:

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

Pada entitas yang Anda butuhkan:

@Column(name = "node_type_code")

Keberuntungan Anda @Converter(autoApply = true)mungkin berbeda di setiap wadah tetapi diuji untuk bekerja pada Wildfly 8.1.0. Jika tidak berhasil, Anda dapat menambahkan @Convert(converter = NodeTypeConverter.class)pada kolom kelas entitas.

Kolam
sumber
"values ​​()" harus "NodeType.values ​​()"
Curtis Yallop
17

Pendekatan terbaik adalah memetakan ID unik untuk setiap jenis enum, sehingga menghindari perangkap ORDINAL dan STRING. Lihat posting ini yang menguraikan 5 cara Anda dapat memetakan enum.

Diambil dari tautan di atas:

1 & 2. Menggunakan @Enumerated

Saat ini ada 2 cara Anda dapat memetakan enum dalam entitas JPA Anda menggunakan penjelasan @Enumerated. Sayangnya baik EnumType.STRING dan EnumType.ORDINAL memiliki keterbatasan.

Jika Anda menggunakan EnumType.String kemudian mengubah nama salah satu tipe enum Anda akan menyebabkan nilai enum Anda tidak sinkron dengan nilai yang disimpan dalam database. Jika Anda menggunakan EnumType.ORDINAL maka menghapus atau menata ulang jenis-jenis dalam enum Anda akan menyebabkan nilai yang disimpan dalam database dipetakan ke jenis enum yang salah.

Kedua opsi ini rapuh. Jika enum dimodifikasi tanpa melakukan migrasi basis data, Anda dapat merusak integritas data Anda.

3. Callback Daur Hidup

Sebuah solusi yang mungkin akan menggunakan anotasi panggilan balik siklus hidup JPA, @PrePersist dan @PostLoad. Ini terasa sangat jelek karena sekarang Anda akan memiliki dua variabel di entitas Anda. Satu memetakan nilai yang disimpan dalam database, dan yang lainnya, enum yang sebenarnya.

4. Memetakan ID unik untuk setiap jenis enum

Solusi yang disukai adalah memetakan enum Anda ke nilai tetap, atau ID, yang ditentukan dalam enum. Memetakan ke nilai yang telah ditentukan dan tetap membuat kode Anda lebih kuat. Setiap modifikasi pada urutan jenis enums, atau refactoring nama, tidak akan menimbulkan efek buruk.

5. Menggunakan Java EE7 @Convert

Jika Anda menggunakan JPA 2.1, Anda memiliki opsi untuk menggunakan anotasi @Convert yang baru. Ini membutuhkan pembuatan kelas konverter, dijelaskan dengan @Converter, di mana Anda akan menentukan nilai apa yang disimpan ke dalam database untuk setiap jenis enum. Di dalam entitas Anda, Anda kemudian akan membubuhi keterangan enum Anda dengan @Convert.

Preferensi saya: (Nomor 4)

Alasan mengapa saya lebih suka mendefinisikan ID saya di dalam enum sebagai lawan menggunakan konverter, adalah enkapsulasi yang baik. Hanya tipe enum yang harus tahu ID-nya, dan hanya entitas yang harus tahu tentang cara memetakan enum ke database.

Lihat posting asli untuk contoh kode.

Chris Ritchie
sumber
1
javax.persistence.Converter sederhana dan dengan @Converter (autoApply = true) memungkinkan untuk menjaga kelas domain bebas dari penjelasan @Convert
Rostislav Matl
9

Masalahnya adalah, saya pikir, bahwa JPA tidak pernah dibentuk dengan gagasan dalam pikiran bahwa kita dapat memiliki Skema yang sudah ada sebelumnya yang sudah ada.

Saya pikir ada dua kelemahan utama yang dihasilkan dari ini, khusus untuk Enum:

  1. Batasan menggunakan nama () dan ordinal (). Mengapa tidak menandai getter dengan @Id, seperti yang kita lakukan dengan @Entity?
  2. Enum biasanya memiliki perwakilan dalam database untuk memungkinkan asosiasi dengan semua jenis metadata, termasuk nama yang tepat, nama deskriptif, mungkin sesuatu dengan lokalisasi dll. Kita membutuhkan kemudahan penggunaan Enum yang dikombinasikan dengan fleksibilitas Entitas.

Bantu perjuangan saya dan pilih JPA_SPEC-47

Bukankah ini lebih elegan daripada menggunakan @Converter untuk menyelesaikan masalah?

// 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.
sumber
4

Kemungkinan kode terkait Pascal terkait

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

}
Rafiq
sumber
3

Saya akan melakukan hal berikut:

Nyatakan secara terpisah enum, dalam file miliknya sendiri:

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


}

Deklarasikan entitas JPA baru bernama Kanan

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


}

Anda juga akan memerlukan konverter untuk menerima nilai-nilai ini (hanya JPA 2.1 dan ada masalah yang tidak akan saya diskusikan di sini dengan enum ini untuk secara langsung tetap menggunakan konverter, sehingga hanya akan menjadi jalan satu arah saja)

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

}

Entitas Otoritas:

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

}

Dengan melakukan cara ini, Anda tidak dapat mengatur langsung ke bidang enum. Namun, Anda dapat mengatur bidang Kanan di Otoritas menggunakan

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

Dan jika Anda perlu membandingkan, Anda dapat menggunakan:

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

Yang cukup keren, saya pikir. Itu tidak sepenuhnya benar, karena konverter itu tidak dimaksudkan untuk digunakan dengan enum. Sebenarnya, dokumentasi mengatakan untuk tidak pernah menggunakannya untuk tujuan ini, Anda harus menggunakan anotasi @Enumerated sebagai gantinya. Masalahnya adalah bahwa hanya ada dua jenis enum: ORDINAL atau STRING, tetapi ORDINAL itu rumit dan tidak aman.


Namun, jika itu tidak memuaskan Anda, Anda dapat melakukan sesuatu yang sedikit lebih rumit dan sederhana (atau tidak).

Ayo lihat.

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


}

dan entitas Otoritas

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

}

Dalam ide kedua ini, ini bukan situasi yang sempurna karena kita meretas atribut ordinal, tetapi itu adalah pengkodean yang jauh lebih kecil.

Saya berpikir bahwa spesifikasi JPA harus menyertakan EnumType.ID di mana bidang nilai enum harus dijelaskan dengan beberapa jenis penjelasan @EnumId.

Carlos Cariello
sumber
2

Solusi saya sendiri untuk menyelesaikan pemetaan Enum JPA semacam ini adalah sebagai berikut.

Langkah 1 - Tulis antarmuka berikut yang akan kita gunakan untuk semua enum yang ingin kita petakan ke kolom db:

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

    T getDbVal();

}

Langkah 2 - Menerapkan konverter JPA khusus generik sebagai berikut:

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

}

Kelas ini akan mengonversi nilai enum Eke bidang tipe database T(misalnya String) dengan menggunakangetDbVal() on enum E, dan sebaliknya.

Langkah 3 - Biarkan enum asli mengimplementasikan antarmuka yang kami tentukan di langkah 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;
    }
}

Langkah 4 - Perpanjang konverter langkah 2 untuk Rightenum langkah 3:

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

Langkah 5 - Langkah terakhir adalah untuk membubuhi keterangan bidang dalam entitas sebagai berikut:

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

Kesimpulan

IMHO ini adalah solusi terbersih dan paling elegan jika Anda memiliki banyak enum untuk dipetakan dan Anda ingin menggunakan bidang enum itu sendiri sebagai nilai pemetaan.

Untuk semua enum lain dalam proyek Anda yang memerlukan logika pemetaan yang serupa, Anda hanya perlu mengulangi langkah 3 hingga 5, yaitu:

  • mengimplementasikan antarmuka IDbValue pada enum Anda;
  • memperpanjang EnumDbValueConverter dengan hanya 3 baris kode (Anda juga dapat melakukan ini dalam entitas Anda untuk menghindari membuat kelas yang terpisah);
  • membubuhi keterangan atribut enum dengan @Convertdari javax.persistencepaket.

Semoga ini membantu.

Danilo Cianciulli
sumber
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

kasus pertama ( EnumType.ORDINAL)

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

kasus kedua ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Mahdi Ben Selimene
sumber