Bản đồ enum trong JPA với các giá trị cố định?


192

Tôi đang tìm kiếm những cách khác nhau để lập bản đồ enum bằng JPA. Tôi đặc biệt muốn đặt giá trị số nguyên của từng mục enum và chỉ lưu giá trị số nguyên.

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

Một giải pháp đơn giản là sử dụng chú thích liệt kê với EnumType.ORDINAL:

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

Nhưng trong trường hợp này, JPA ánh xạ chỉ số enum (0,1,2) chứ không phải giá trị tôi muốn (100,200,300).

Hai giải pháp tôi thấy không đơn giản ...

Giải pháp đầu tiên

Một giải pháp, được đề xuất ở đây , sử dụng @PrePersist và @PostLoad để chuyển đổi enum sang một trường khác và đánh dấu trường enum là tạm thời:

@Basic
private int intValueForAnEnum;

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

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

Giải pháp thứ hai

Giải pháp thứ hai được đề xuất ở đây đã đề xuất một đối tượng chuyển đổi chung, nhưng vẫn có vẻ nặng và hướng theo chế độ ngủ đông (@Type dường như không tồn tại trong 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")
            }
)

Có giải pháp nào khác không?

Tôi có một vài ý tưởng trong đầu nhưng tôi không biết liệu chúng có tồn tại trong JPA không:

  • sử dụng các phương thức setter và getter của thành viên bên phải của lớp Author khi tải và lưu đối tượng Author
  • một ý tưởng tương đương sẽ là nói với JPA các phương thức của Right enum để chuyển đổi enum thành int và int thành enum
  • Bởi vì tôi đang sử dụng Spring, có cách nào để nói với JPA sử dụng một trình chuyển đổi cụ thể (RightEditor) không?

7
Thật kỳ lạ khi sử dụng ORDINAL đôi khi ai đó sẽ thay đổi vị trí của các mục trong bảng liệt kê và cơ sở dữ liệu sẽ trở thành thảm họa
Natasha KP

2
sẽ không áp dụng tương tự cho việc sử dụng Tên - ai đó có thể thay đổi tên enum và họ lại không đồng bộ với cơ sở dữ liệu ...
topchef

2
Tôi đồng ý với @NatashaKP. Đừng sử dụng thứ tự. Để thay đổi tên, không có điều đó. Bạn thực sự đang xóa enum cũ và thêm một cái mới với một tên mới, vì vậy, có, mọi dữ liệu được lưu trữ sẽ không đồng bộ (ngữ nghĩa, có lẽ: P).
Svend Hansen

Có 5 giải pháp mà tôi biết. Xem câu trả lời của tôi dưới đây, nơi tôi có một câu trả lời chi tiết.
Chris Ritchie

Câu trả lời:


168

Đối với các phiên bản sớm hơn JPA 2.1, JPA chỉ cung cấp hai cách để đối phó với enum, bằng cách của họ namehoặc bằng cách của họ ordinal. Và JPA tiêu chuẩn không hỗ trợ các loại tùy chỉnh. Vì thế:

  • Nếu bạn muốn thực hiện chuyển đổi loại tùy chỉnh, bạn sẽ phải sử dụng tiện ích mở rộng của nhà cung cấp (với Hibernate UserType, EclipseLink Converter, v.v.). (giải pháp thứ hai). ~ hoặc ~
  • Bạn sẽ phải sử dụng thủ thuật @PrePersist và @PostLoad (giải pháp đầu tiên). ~ hoặc ~
  • Chú thích getter và setter lấy và trả về intgiá trị ~ hoặc ~
  • Sử dụng một thuộc tính số nguyên ở cấp thực thể và thực hiện dịch trong getters và setters.

Tôi sẽ minh họa tùy chọn mới nhất (đây là một triển khai cơ bản, điều chỉnh nó theo yêu cầu):

@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
Thật đáng buồn khi JPA không có hỗ trợ riêng cho việc này
Jaime Hablutzel

20
@jaime Đồng ý! Có điên không khi nghĩ rằng các nhà phát triển có thể muốn duy trì một enum là giá trị của một trong các trường / thuộc tính của nó thay vì giá trị hoặc tên int của nó? Cả hai đều cực kỳ "mong manh" và tái cấu trúc - không thân thiện. Và sử dụng tên cũng giả sử bạn đang sử dụng cùng một quy ước đặt tên trong cả Java và cơ sở dữ liệu. Lấy giới tính làm ví dụ. Điều đó có thể được định nghĩa đơn giản là 'M' hoặc 'F' trong cơ sở dữ liệu nhưng điều đó không ngăn tôi sử dụng Giới tính.MALE và Giới tính.FEMALE trong Java, thay vì Giới tính.M hoặc Giới tính.F.
spaaarky21

2
Tôi đoán một lý do có thể là cả tên và thứ tự đều được đảm bảo là duy nhất, trong khi bất kỳ giá trị hoặc trường nào khác thì không. Đúng là thứ tự có thể thay đổi (không sử dụng thứ tự) và tên cũng có thể thay đổi (không thay đổi tên enum: P), nhưng cũng có thể có bất kỳ giá trị nào khác ... Tôi không chắc là tôi thấy giá trị lớn với việc thêm khả năng lưu trữ một giá trị khác.
Svend Hansen

2
Trên thực tế, tôi thấy giá trị ... Tôi sẽ xóa phần bình luận đó ở trên, nếu tôi vẫn có thể chỉnh sửa nó: P: D
Svend Hansen

13
JPA2.1 sẽ có bộ chuyển đổi hỗ trợ nguyên bản. Vui lòng xem somhoughtsonjava.blogspot.fi/2013/10/ từ
drodil

69

Điều này hiện có thể với JPA 2.1:

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

Biết thêm chi tiết:


2
Những gì bây giờ có thể? Chắc chắn chúng ta có thể sử dụng @Converter, nhưng enumnên được xử lý thanh lịch hơn ra khỏi hộp!
YoYo

4
"Trả lời" tham chiếu 2 liên kết nói về việc sử dụng AttributeConverterBUT trích dẫn một số mã không có gì thuộc loại này và không trả lời OP.

@ DN1 cứ thoải mái cải thiện nó
Tvaroh

1
Đó là "câu trả lời" của bạn, vì vậy bạn nên "cải thiện nó". Đã có câu trả lời choAttributeConverter

1
Hoàn hảo làm việc sau khi thêm:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal28

22

Từ JPA 2.1, bạn có thể sử dụng AttributionConverter .

Tạo một lớp liệt kê như vậy:

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

Và tạo một trình chuyển đổi như thế này:

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

Trên thực thể bạn chỉ cần:

@Column(name = "node_type_code")

Bạn may mắn có @Converter(autoApply = true)thể thay đổi theo container nhưng được thử nghiệm để hoạt động trên Wildfly 8.1.0. Nếu nó không hoạt động, bạn có thể thêm @Convert(converter = NodeTypeConverter.class)vào cột lớp thực thể.


"giá trị ()" phải là "NodeType.values ​​()"
Curtis Yallop

17

Cách tiếp cận tốt nhất sẽ là ánh xạ một ID duy nhất cho từng loại enum, do đó tránh được những cạm bẫy của ORDINAL và STRING. Xem bài đăng này trong đó nêu ra 5 cách bạn có thể lập bản đồ.

Lấy từ liên kết trên:

1 & 2. Sử dụng @Enatedated

Hiện tại có 2 cách bạn có thể ánh xạ enum trong các thực thể JPA của mình bằng cách sử dụng chú thích @Enumerated. Thật không may, cả EnumType.STRING và EnumType.ORDINAL đều có những hạn chế.

Nếu bạn sử dụng EnumType.String thì việc đổi tên một trong các loại enum của bạn sẽ khiến giá trị enum của bạn không đồng bộ với các giá trị được lưu trong cơ sở dữ liệu. Nếu bạn sử dụng EnumType.ORDINAL thì việc xóa hoặc sắp xếp lại các loại trong enum của bạn sẽ khiến các giá trị được lưu trong cơ sở dữ liệu ánh xạ tới các loại enum sai.

Cả hai tùy chọn này đều dễ vỡ. Nếu enum được sửa đổi mà không thực hiện di chuyển cơ sở dữ liệu, bạn có thể mở lại toàn vẹn dữ liệu của mình.

3. Gọi lại vòng đời

Một giải pháp khả thi sẽ sử dụng các chú thích gọi lại vòng đời JPA, @PrePersist và @PostLoad. Điều này cảm thấy khá xấu xí vì bây giờ bạn sẽ có hai biến trong thực thể của mình. Một ánh xạ giá trị được lưu trữ trong cơ sở dữ liệu, và cái kia, enum thực tế.

4. Ánh xạ ID duy nhất cho từng loại enum

Giải pháp ưa thích là ánh xạ enum của bạn tới một giá trị cố định hoặc ID, được xác định trong enum. Ánh xạ tới giá trị cố định, được xác định trước làm cho mã của bạn mạnh mẽ hơn. Bất kỳ sửa đổi nào theo thứ tự của các loại enums, hoặc tái cấu trúc các tên, sẽ không gây ra bất kỳ tác động bất lợi nào.

5. Sử dụng Java EE7 @Convert

Nếu bạn đang sử dụng JPA 2.1, bạn có tùy chọn sử dụng chú thích @Convert mới. Điều này yêu cầu tạo ra một lớp trình chuyển đổi, được chú thích bằng @Converter, trong đó bạn sẽ xác định giá trị nào được lưu vào cơ sở dữ liệu cho từng loại enum. Trong thực thể của bạn, sau đó bạn sẽ chú thích enum của bạn với @Convert.

Sở thích của tôi: (Số 4)

Lý do tại sao tôi thích xác định ID của mình trong enum thay vì sử dụng trình chuyển đổi, là đóng gói tốt. Chỉ loại enum mới biết ID của nó và chỉ thực thể mới biết về cách nó ánh xạ enum vào cơ sở dữ liệu.

Xem bài viết gốc cho ví dụ mã.


1
javax.persistence.Converter rất đơn giản và với @Converter (autoApply = true), nó cho phép giữ các lớp miền không có chú thích @Convert
Rostislav

9

Vấn đề là, tôi nghĩ rằng JPA chưa bao giờ được biết đến với ý tưởng rằng chúng ta có thể có một Schema tồn tại phức tạp đã có sẵn.

Tôi nghĩ có hai thiếu sót chính dẫn đến việc này, cụ thể là Enum:

  1. Giới hạn của việc sử dụng tên () và thứ tự (). Tại sao không đánh dấu một getter bằng @Id, cách chúng ta làm với @Entity?
  2. Enum thường có đại diện trong cơ sở dữ liệu để cho phép liên kết với tất cả các loại siêu dữ liệu, bao gồm tên riêng, tên mô tả, có thể là thứ gì đó với bản địa hóa, v.v. Chúng tôi cần dễ dàng sử dụng Enum kết hợp với tính linh hoạt của Thực thể.

Giúp tôi nguyên nhân và bỏ phiếu trên JPAinksEC-47

Điều này sẽ không thanh lịch hơn việc sử dụng @Converter để giải quyết vấn đề?

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

Có thể đóng mã liên quan của 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

Tôi sẽ làm mọi người:

Khai báo tách riêng enum, trong tập tin riêng của nó:

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


}

Khai báo một thực thể JPA mới có tên 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);
    }


}

Bạn cũng sẽ cần một trình chuyển đổi để nhận các giá trị này (chỉ có JPA 2.1 và có vấn đề mà tôi sẽ không thảo luận ở đây với các enum này để được duy trì trực tiếp bằng cách sử dụng trình chuyển đổi, vì vậy nó sẽ chỉ là đường một chiều)

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

}

Thực thể quyền:

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

}

Bằng cách này, bạn có thể đặt trực tiếp vào trường enum. Tuy nhiên, bạn có thể đặt trường Đúng trong Quyền bằng cách sử dụng

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

Và nếu bạn cần so sánh, bạn có thể sử dụng:

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

Đó là khá mát mẻ, tôi nghĩ. Nó không hoàn toàn chính xác, vì bộ chuyển đổi mà nó không có ý định sử dụng với enum. Trên thực tế, tài liệu nói rằng không bao giờ sử dụng nó cho mục đích này, bạn nên sử dụng chú thích @Enumerated thay thế. Vấn đề là chỉ có hai loại enum: ORDINAL hoặc STRING, nhưng ORDINAL là khó khăn và không an toàn.


Tuy nhiên, nếu nó không làm bạn hài lòng, bạn có thể làm điều gì đó đơn giản hơn một chút và đơn giản hơn (hoặc không).

Hãy xem nào.

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


}

và thực thể quyền

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

}

Trong ý tưởng thứ hai này, nó không phải là một tình huống hoàn hảo vì chúng tôi hack thuộc tính thứ tự, nhưng nó mã hóa nhỏ hơn nhiều.

Tôi nghĩ rằng đặc tả JPA nên bao gồm EnumType.ID trong đó trường giá trị enum nên được chú thích với một số loại chú thích @EnumId.


2

Giải pháp của riêng tôi để giải quyết loại ánh xạ Enum JPA này là như sau.

Bước 1 - Viết giao diện sau mà chúng ta sẽ sử dụng cho tất cả các enum mà chúng ta muốn ánh xạ tới cột db:

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

    T getDbVal();

}

Bước 2 - Thực hiện một trình chuyển đổi JPA chung tùy chỉnh như sau:

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

}

Lớp này sẽ chuyển đổi giá trị enum Ethành trường cơ sở dữ liệu loại T(ví dụ String) bằng cách sử dụng getDbVal()enum trên Evà ngược lại.

Bước 3 - Để enum ban đầu thực hiện giao diện mà chúng ta đã xác định trong bước 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;
    }
}

Bước 4 - Mở rộng trình chuyển đổi của bước 2 cho Rightenum của bước 3:

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

Bước 5 - Bước cuối cùng là chú thích trường trong thực thể như sau:

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

Phần kết luận

IMHO đây là giải pháp sạch nhất và thanh lịch nhất nếu bạn có nhiều enum để ánh xạ và bạn muốn sử dụng một trường cụ thể của chính enum làm giá trị ánh xạ.

Đối với tất cả các enum khác trong dự án của bạn cần logic ánh xạ tương tự, bạn chỉ phải lặp lại các bước từ 3 đến 5, đó là:

  • thực hiện giao diện IDbValuetrên enum của bạn;
  • mở rộng EnumDbValueConverterchỉ với 3 dòng mã (bạn cũng có thể thực hiện điều này trong thực thể của mình để tránh tạo một lớp riêng biệt);
  • chú thích thuộc tính enum với @Converttừ javax.persistencegói.

Hi vọng điêu nay co ich.


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

trường hợp đầu tiên ( EnumType.ORDINAL)

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

trường hợp thứ hai ( EnumType.STRING)

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.