JPA'da sabit değerlerle enum eşleştirilsin mi?


192

JPA kullanarak bir numaralandırma eşlemek için farklı yollar arıyorum. Özellikle her numaralandırma girişinin tamsayı değerini ayarlamak ve sadece tamsayı değerini kaydetmek istiyorum.

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

Basit bir çözüm EnumType ile Numaralandırılmış ek açıklamayı kullanmaktır: ORDINAL:

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

Ancak bu durumda JPA, istediğim değeri (100,200,300) değil, numaralandırma dizinini (0,1,2) eşler.

Bulduğum iki çözüm basit görünmüyor ...

İlk Çözüm

Burada önerilen bir çözüm, numaralandırmayı başka bir alana dönüştürmek ve numaralandırma alanını geçici olarak işaretlemek için @PrePersist ve @PostLoad kullanır:

@Basic
private int intValueForAnEnum;

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

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

İkinci Çözüm

Burada önerilen ikinci çözüm genel bir dönüşüm nesnesi önerdi, ancak yine de ağır ve hazırda bekletme odaklı görünüyor (@Type, Java EE'de mevcut görünmüyor):

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

Başka çözüm var mı?

Aklımda birkaç fikir var ama JPA'da var olup olmadıklarını bilmiyorum:

  • Authority nesnesini yüklerken ve kaydederken Authority Class'ın sağ üyesinin ayarlayıcı ve alıcı yöntemlerini kullanın
  • eşdeğer bir fikir, JPA'ya enum'u int ve int'e enum'a dönüştürmek için Sağ numaralandırma yöntemlerinin neler olduğunu söylemek olacaktır.
  • Spring kullandığım için, JPA'ya belirli bir dönüştürücü (RightEditor) kullanmasını söylemenin bir yolu var mı?

7
ORDINAL'ın bazen numaralandırmadaki öğelerin yerlerini değiştireceği ve veritabanının felaket olacağı gariptir
Natasha KP

2
aynı isim kullanmak için geçerli olmaz - birisi numaralandırma ad (lar) ı değiştirebilir ve yine veritabanı ile senkronize değildir ...
topchef

2
@NatashaKP ile hemfikirim. Ordinal kullanmayın. İsmi değiştirmek için böyle bir şey yok. Aslında eski numaralandırmayı siliyor ve yeni bir adla yeni bir tane ekliyorsunuz, bu nedenle evet, saklanan veriler senkronize olmayacak (anlambilim, belki de P).
Svend Hansen

Evet bildiğim 5 çözüm var. Ayrıntılı bir cevabımın olduğu aşağıdaki cevabımı görün.
Chris Ritchie

Yanıtlar:


168

JPA 2.1'den önceki sürümler için JPA, numaralandırmalarla kendileri nameveya kendileri tarafından başa çıkmak için yalnızca iki yol sağlar ordinal. Ve standart JPA özel türleri desteklemez. Yani:

  • Özel tür dönüşümleri yapmak istiyorsanız, bir sağlayıcı uzantısı kullanmanız gerekir (Hazırda Beklet UserType, EclipseLink Convertervb. İle ). (ikinci çözüm). • ya da ~
  • @PrePersist ve @PostLoad hile (ilk çözüm) kullanmanız gerekir. • ya da ~
  • Alıcıya ve ayarlayıcıya int~ veya ~ değerini alarak ve döndürerek açıklama ekleyin
  • Varlık düzeyinde bir tamsayı özniteliği kullanın ve alıcılarda ve ayarlayıcılarda bir çeviri gerçekleştirin.

En son seçeneği göstereceğim (bu temel bir uygulamadır, gerektiği gibi değiştirin):

@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
Çok üzücü
JPA'nın

20
@jaime Kabul etti! Geliştiricilerin, int değeri veya adı yerine alanlarından / özelliklerinden birinin değeri olarak bir enum sürdürmek isteyebileceğini düşünmek çılgın mı? Her ikisi de son derece "kırılgan" ve yeniden düzenleyici-düşmanca. Ayrıca adı kullanmak, hem Java'da hem de veritabanında aynı adlandırma kuralını kullandığınızı varsayar. Örneğin cinsiyeti ele alalım. Bu, veritabanında basitçe 'M' veya 'F' olarak tanımlanabilir, ancak Java'da Gender.M veya Gender.F yerine Gender.MALE ve Gender.FEMALE kullanmamı engellememelidir.
spaaarky21

2
Sanırım bir sebep, isim ve ordinalin benzersiz olduğu garanti edilirken, diğer değerler veya alanlar böyle olmayabilir. Siparişin değişebileceği (sıra kullanmayın) ve ad da değiştirilebileceği doğrudur (numaralandırma adlarını değiştirmeyin: P), ancak başka bir değer de olabilir ... Gördüğümden emin değilim farklı bir değer depolama yeteneği ekleyerek büyük değer.
Svend Hansen

2
Aslında, değeri görüyorum ... Yine de düzenleyebilirsem yukarıdaki yorumumun bir kısmını silebilirdim: P: D
Svend Hansen

13
JPA2.1, yerel olarak dönüştürücü desteğine sahip olacaktır. Lütfen bkz somethoughtsonjava.blogspot.fi/2013/10/…
drodil

69

Bu artık JPA 2.1 ile mümkün:

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

Daha fazla detay:


2
Şimdi ne mümkün? Tabii ki kullanabiliriz @Converter, ancak enumkutudan daha zarif bir şekilde ele alınmalıdır!
YoYo

4
"Cevap" bir AttributeConverterAMA kullanmaktan bahseden 2 bağlantıya atıfta bulunarak , hiçbir şey yapmayan ve OP'ye cevap vermeyen bazı kodlardan alıntı yapar.

@ DN1 bunu geliştirmekten çekinmeyin
Tvaroh

1
Bu sizin "cevabınız", bu yüzden "geliştirmelisiniz". Zaten bir cevabı varAttributeConverter

1
@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Ekledikten

23

JPA 2.1'den AttributeConverter kullanabilirsiniz .

Aşağıdaki gibi numaralandırılmış bir sınıf oluşturun:

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

Ve bunun gibi bir dönüştürücü oluşturun:

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

Varlıkta sadece ihtiyacınız olan:

@Column(name = "node_type_code")

Şansınız @Converter(autoApply = true)konteynere göre değişebilir, ancak Wildfly 8.1.0 üzerinde çalıştığı test edilmiştir. Eğer işe yaramazsa @Convert(converter = NodeTypeConverter.class)varlık sınıfı sütununa ekleyebilirsiniz .


"değerleri ()" "NodeType.values ​​()" olmalıdır
Curtis Yallop

17

En iyi yaklaşım, her bir enum tipine benzersiz bir kimlik eşleştirmek ve böylece ORDINAL ve STRING'in tuzaklarından kaçınmak olacaktır. Bu bak yazıyı 5 yolu Bir enum eşleyebilirsiniz özetliyor.

Yukarıdaki bağlantıdan alınmıştır:

1 ve 2. @Enumerated öğesini kullanma

@Enumerated ek açıklamasını kullanarak JPA öğelerinizdeki sıralamaları eşlemenin 2 yolu vardır. Ne yazık ki, hem EnumType.STRING hem de EnumType.ORDINAL sınırlamaları vardır.

EnumType.String komutunu kullanırsanız, numaralandırma türlerinizden birini yeniden adlandırmak, numaralandırma değerinizin veritabanına kaydedilen değerlerle eşitlenmemesine neden olur. EnumType.ORDINAL kullanırsanız, numaralandırmanızdaki türleri silmek veya yeniden sıralamak veritabanına kaydedilen değerlerin yanlış numaralandırma türleriyle eşleşmesine neden olur.

Bu seçeneklerin her ikisi de kırılgandır. Numaralandırma veritabanı geçişi yapılmadan değiştirilirse, verilerinizin bütünlüğünü tehlikeye atabilirsiniz.

3. Yaşam Döngüsü Geri Aramaları

Olası bir çözüm, JPA yaşam döngüsü geri arama ek açıklamalarını, @PrePersist ve @PostLoad kullanmak olacaktır. Şimdi varlığınızda iki değişken olacak gibi bu oldukça çirkin bir his veriyor. Biri veritabanında depolanan değeri ve diğeri de gerçek numarayı eşler.

4. Benzersiz numarayı her numaralandırma türüyle eşleme

Tercih edilen çözüm, numaralandırmanızı, numaralandırma içinde tanımlanan sabit bir değere veya kimliğe eşlemektir. Önceden tanımlanmış, sabit bir değerle eşleme, kodunuzu daha sağlam hale getirir. Numaralandırma türlerinin düzeninde yapılan herhangi bir değişiklik veya adların yeniden düzenlenmesi, herhangi bir olumsuz etkiye neden olmaz.

5. Java EE7 @Convert Kullanma

JPA 2.1 kullanıyorsanız, yeni @ Convert ek açıklamasını kullanma seçeneğiniz vardır. Bu, her bir numaralandırma türü için hangi değerlerin veritabanına kaydedileceğini tanımlayacağınız @Converter ile açıklamalı bir dönüştürücü sınıfı oluşturulmasını gerektirir. Daha sonra varlığınız içinde enumunuza @Convert ile açıklama eklersiniz.

Benim tercihim: (Sayı 4)

Bir dönüştürücü kullanmak yerine numaramdaki kimliğimi tanımlamayı tercih etmemin nedeni iyi bir kapsülleme. Sadece enum türü kimliğini bilmeli ve yalnızca kuruluş enum'u veritabanıyla nasıl eşlediğini bilmelidir.

Kod örneği için orijinal gönderiye bakın .


1
javax.persistence.Converter basittir ve @Converter (autoApply = true) ile alan adı sınıflarının @Convert ek açıklamalarından uzak tutulmasına izin verir
Rostislav Matl

9

Sorun, bence, JPA'nın önceden var olan karmaşık bir önceden varolan Şemaya sahip olabileceğimiz düşüncesiyle asla engellenmediğidir.

Enum'a özgü olarak bundan kaynaklanan iki ana eksiklik olduğunu düşünüyorum:

  1. Name () ve ordinal () kullanımının sınırlandırılması. Neden bir alıcıyı @Int ile işaretlemiyoruz, @Entity ile yaptığımız gibi?
  2. Enum'lar genellikle, uygun bir ad, açıklayıcı bir ad, belki de yerelleştirme vb. Dahil olmak üzere her türlü meta veriyle ilişkilendirmeye izin vermek için veritabanında temsil edilir.

Sebebime yardım et ve JPA_SPEC-47'ye oy ver

Bu, sorunu çözmek için bir @Converter kullanmaktan daha şık olmaz mıydı?

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

Muhtemelen ilgili Pascal kodunu kapat

@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

Ben aşağıdakileri yapardım:

Numarayı ayrı ayrı kendi dosyasında bildir:

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


}

Right adlı yeni bir JPA varlığı bildirin

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


}

Ayrıca bu değerleri almak için bir dönüştürücüye ihtiyacınız olacak (sadece JPA 2.1 ve burada bir sorun var, dönüştürücü kullanarak doğrudan kalıcı olması için bu numaralarla burada tartışmayacağım, bu yüzden sadece tek yönlü bir yol olacak)

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

}

Kurum tüzel kişiliği:

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

}

Bu şekilde, doğrudan numaralandırma alanına ayarlanamaz. Ancak, Yetki'deki Doğru alanını kullanarak

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

Ve karşılaştırmanız gerekiyorsa, şunları kullanabilirsiniz:

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

Sanırım oldukça havalı. Bu tamamen doğru değil, çünkü dönüştürücü sayımlarla birlikte kullanılması amaçlanmadı. Aslında, belgeler bu amaç için asla kullanılmayacağını söylüyor, bunun yerine @Enumerated ek açıklamasını kullanmalısınız. Sorun sadece iki numara türü vardır: ORDINAL veya STRING, ancak ORDINAL zor ve güvenli değil.


Ancak, sizi tatmin etmiyorsa, biraz daha hacky ve daha basit bir şey yapabilirsiniz (veya değil).

Bakalım.

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


}

ve Kurum tüzel kişiliği

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

}

Bu ikinci fikirde, sıradan niteliği hacklediğimiz için mükemmel bir durum değil, ama çok daha küçük bir kodlama.

JPA belirtimi enumType.ID burada numaralandırma alanı @EnumId ek açıklama bir tür açıklamalı olması gerektiğini düşünüyorum.


2

Bu tür Enum JPA eşlemesini çözmek için kendi çözümüm şudur.

Adım 1 - Bir db sütununa eşlemek istediğimiz tüm numaralandırmalar için kullanacağımız aşağıdaki arayüzü yazın:

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

    T getDbVal();

}

Adım 2 - Özel bir genel JPA dönüştürücüyü aşağıdaki gibi uygulayın:

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

}

Bu sınıf enum değer dönüştürür Etipte bir veri tabanı alanında T(örneğin Stringkullanarak) getDbVal()enum ile Etersine, ve aksi.

3. Adım - Orijinal numaralandırmanın 1. adımda tanımladığımız arayüzü uygulamasına izin verin:

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

Adım 4 - RightAdım 3'teki numaralandırma için adım 2'nin dönüştürücüsünü uzatın :

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

Adım 5 - Son adım, varlıktaki alana aşağıdaki gibi açıklama eklemektir:

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

Sonuç

IMHO, eşleştirilecek çok sayıda numaranız varsa ve enumun belirli bir alanını eşleme değeri olarak kullanmak istiyorsanız, bu en temiz ve en zarif çözümdür.

Projenizde benzer eşleme mantığına ihtiyaç duyan diğer tüm numaralandırmalar için yalnızca 3'ten 5'e kadar olan adımları tekrarlamanız gerekir, yani:

  • arayüzü IDbValuenumaralandırmanıza uygulayın;
  • EnumDbValueConverteryalnızca 3 satırlık kodla genişlet (ayrı bir sınıf oluşturmamak için bunu kuruluşunuz içinde de yapabilirsiniz);
  • ile enum niteliğini açıklama @Convertdan javax.persistencepaketin.

Bu yardımcı olur umarım.


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

ilk durum ( EnumType.ORDINAL)

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

ikinci durum ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.