Is it possible to write a generic enum converter for JPA?

后端 未结 4 1734
萌比男神i
萌比男神i 2020-12-01 04:51

I wanted to write a Converter for JPA that stores any enum as UPPERCASE. Some enums we encounter do not follow yet the convention to use only Uppercase letters so until they

相关标签:
4条回答
  • 2020-12-01 05:07

    What you need to do is write a generic base class and then extend that for each enum type you want to persist. Then use the extended type in the @Converter annotation:

    public abstract class GenericEnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {
        ...
    }
    
    public FooConverter
        extends GenericEnumUppercaseConverter<Foo> 
        implements AttributeConverter<Foo, String> // See Bug HHH-8854
    {
        public FooConverter() {
            super(Foo.class);
        }
    }
    

    where Foo is the enum you want to handle.

    The alternative would be to define a custom annotation, patch the JPA provider to recognize this annotation. That way, you could examine the field type as you build the mapping information and feed the necessary enum type into a purely generic converter.

    Related:

    • https://hibernate.atlassian.net/browse/HHH-8854
    0 讨论(0)
  • 2020-12-01 05:11

    Based on @scottb solution I made this, tested against hibernate 4.3: (no hibernate classes, should run on JPA just fine)

    Interface enum must implement:

    public interface PersistableEnum<T> {
        public T getValue();
    }
    

    Base abstract converter:

    @Converter
    public abstract class AbstractEnumConverter<T extends Enum<T> & PersistableEnum<E>, E> implements AttributeConverter<T, E> {
        private final Class<T> clazz;
    
        public AbstractEnumConverter(Class<T> clazz) {
            this.clazz = clazz;
        }
    
        @Override
        public E convertToDatabaseColumn(T attribute) {
            return attribute != null ? attribute.getValue() : null;
        }
    
        @Override
        public T convertToEntityAttribute(E dbData) {
            T[] enums = clazz.getEnumConstants();
    
            for (T e : enums) {
                if (e.getValue().equals(dbData)) {
                    return e;
                }
            }
    
            throw new UnsupportedOperationException();
        }
    }
    

    You must create a converter class for each enum, I find it easier to create static class inside the enum: (jpa/hibernate could just provide the interface for the enum, oh well...)

    public enum IndOrientation implements PersistableEnum<String> {
        LANDSCAPE("L"), PORTRAIT("P");
    
        private final String value;
    
        @Override
        public String getValue() {
            return value;
        }
    
        private IndOrientation(String value) {
            this.value= value;
        }
    
        public static class Converter extends AbstractEnumConverter<IndOrientation, String> {
            public Converter() {
                super(IndOrientation.class);
            }
        }
    }
    

    And mapping example with annotation:

    ...
    @Convert(converter = IndOrientation.Converter.class)
    private IndOrientation indOrientation;
    ...
    

    With some changes you can create a IntegerEnum interface and generify for that.

    0 讨论(0)
  • 2020-12-01 05:14

    My solution to this problem looks similar and also makes use of the JPA 2.1 Converter facility. Alas, generic types in Java 8 are not reified, and so there does not appear to be an easy way to avoid writing a separate class for each Java enum that you want to be able to convert to/from a database format.

    You can however reduce the process of writing an enum converter class to pure boilerplate. The components of this solution are:

    1. Encodeable interface; the contract for an enum class that grants access to a String token for each enum constant (also a factory for getting the enum constant for a matching token)
    2. AbstractEnumConverter class; provides the common code for translating tokens to/from enum constants
    3. Java enum classes that implement the Encodeable interface
    4. JPA converter classes that extend the AbstractEnumConverter class

    The Encodeable interface is simple and contains a static factory method, forToken(), for obtaining enum constants:

    public interface Encodeable {
    
        String token();
    
        public static <E extends Enum<E> & Encodeable> E forToken(Class<E> cls, String tok) {
            final String t = tok.trim().toUpperCase();
            return Stream.of(cls.getEnumConstants())
                    .filter(e -> e.token().equals(t))
                    .findFirst()
                    .orElseThrow(() -> new IllegalArgumentException("Unknown token '" +
                            tok + "' for enum " + cls.getName()));
        }
    }
    

    The AbstractEnumConverter class is a generic class that is also simple. It implements the JPA 2.1 AttributeConverter interface but provides no implementations for its methods (because this class can't know the concrete types needed for obtaining the appropriate enum constants). Instead, it defines helper methods that the concrete converter classes will chain to:

    public abstract class AbstractEnumConverter<E extends Enum<E> & Encodeable>
                implements AttributeConverter<E, String> {
    
        public String toDatabaseColumn(E attr) {
            return (attr == null)
                    ? null
                    : attr.token();
        }
    
        public E toEntityAttribute(Class<E> cls, String dbCol) {
            return (dbCol == null)
                    ? null
                    : Encodeable.forToken(cls, dbCol);
        }
    }
    

    An example of a concrete enum class that could now be persisted to a database with the JPA 2.1 Converter facility is shown below (note that it implements Encodeable, and that the token for each enum constant is defined as a private field):

    public enum GenderCode implements Encodeable {
    
        MALE   ("M"), 
        FEMALE ("F"), 
        OTHER  ("O");
    
        final String e_token;
    
        GenderCode(String v) {
            this.e_token = v;
        }
    
        @Override
        public String token() {
            return this.e_token;
        }
    }
    

    The boilerplate for every JPA 2.1 Converter class would now look like this (note that every such converter will need to extend AbstractEnumConverter and provide implementations for the methods of the JPA AttributeConverter interface):

    @Converter
    public class GenderCodeConverter 
                extends AbstractEnumConverter<GenderCode> {
    
        @Override
        public String convertToDatabaseColumn(GenderCode attribute) {
            return this.toDatabaseColumn(attribute);
        }
    
        @Override
        public GenderCode convertToEntityAttribute(String dbData) {
            return this.toEntityAttribute(GenderCode.class, dbData);
        }
    }
    
    0 讨论(0)
  • 2020-12-01 05:19

    The above solutions are really fine. My small additions here.

    I also added the following to enforce when implementing the interface writing a converter class. When you forget jpa starts using default mechanisms which are really fuzzy solutions (especially when mapping to some number value, which I always do).

    The interface class looks like this:

    public interface PersistedEnum<E extends Enum<E> & PersistedEnum<E>> {
      int getCode();
      Class<? extends PersistedEnumConverter<E>> getConverterClass();
    }
    

    With the PersistedEnumConverter similar to previous posts. However when the implementing this interface you have to deal with the getConverterClass implementation, which is, besides being an enforcement to provide the specific converter class, completely useless.

    Here is an example implementation:

    public enum Status implements PersistedEnum<Status> {
      ...
    
      @javax.persistence.Converter(autoApply = true)
      static class Converter extends PersistedEnumConverter<Status> {
          public Converter() {
              super(Status.class);
          }
      }
    
      @Override
      public Class<? extends PersistedEnumConverter<Status>> getConverterClass() {
          return Converter.class;
      }
    
      ...
    }
    

    And what I do in the database is always make a companion table per enum with a row per enum value

     create table e_status
        (
           id    int
               constraint pk_status primary key,
           label varchar(100)
        );
    
      insert into e_status
      values (0, 'Status1');
      insert into e_status
      values (1, 'Status2');
      insert into e_status
      values (5, 'Status3');
    

    and put a fk constraint from wherever the enum type is used. Like this the usage of correct enum values is always guaranteed. I especially put values 0, 1 and 5 here to show how flexible it is, and still solid.

    create table using_table
       (
            ...
        status         int          not null
            constraint using_table_status_fk references e_status,
            ...
       );
    
    0 讨论(0)
提交回复
热议问题