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

7

46

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 are refactored I still store the future value.

What I got so far:

package student;

public enum StudentState {

    Started,
    Mentoring,
    Repeating,
    STUPID,
    GENIUS;
}

I want "Started" to be stored as "STARTED" and so on.

package student;

import jpa.EnumUppercaseConverter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "STUDENTS")
public class Student implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    @Column(name = "LAST_NAME", length = 35)
    private String mLastName;

    @Column(name = "FIRST_NAME", nullable = false, length = 35)
    private String mFirstName;

    @Column(name = "BIRTH_DATE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date mBirthDate;

    @Column(name = "STUDENT_STATE")
    @Enumerated(EnumType.STRING)
    @Convert(converter = EnumUppercaseConverter.class)
    private StudentState studentState;

}

the converter currently looks like this:

package jpa;


import javax.persistence.AttributeConverter;
import java.util.EnumSet;

public class EnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {

    private Class<E> enumClass;

    @Override
    public String convertToDatabaseColumn(E e) {
        return e.name().toUpperCase();
    }

    @Override
    public E convertToEntityAttribute(String s) {
        // which enum is it?
        for (E en : EnumSet.allOf(enumClass)) {
            if (en.name().equalsIgnoreCase(s)) {
                return en;
            }
        }
        return null;
    }

}

what will not work is that I do not know what enumClass will be at runtime. And I could not figure out a way to pass this information to the converter in the @Converter annotation.

So is there a way to add parameters to the converter or cheat a bit? Or is there another way?

I'm using EclipseLink 2.4.2

Thanks!

Vapor answered 9/5, 2014 at 12:18 Comment(2)
Beware that this is likely to be fragile, especially because it's perfectly legal for an enum to have values AVALUE and AValue.Trug
yes thats true but I define that as completly forbidden :DVapor
A
25

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:

Alpenstock answered 9/5, 2014 at 12:24 Comment(4)
But, enums can't extend another class? https://mcmap.net/q/373456/-how-to-extend-enum-class-from-abstract-classGaloshes
@pioto: That doesn't matter in this case. I've added an example how to do it.Alpenstock
Ahha! I thought you were talking about an abstract class for the Enum, not the Converter... this looks great, and I'll give it a try. Thanks!Galoshes
This works great, thanks, with one caveat: at least in Hibernate, you need to add implements AttributeConverter <Foo, String> to the declaration of your concrete FooConverter class, thanks to this bug: hibernate.atlassian.net/browse/HHH-8854Galoshes
E
45

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.

Emulsify answered 2/3, 2018 at 20:23 Comment(7)
Hi Sérgio, thanks for your contribution. I implemented your solution and removed @Convert from AbstractEnumConverter because was throwing an exception about ParameterizedType. After that everything works. Thank you.Trashy
You are welcome. I do have that same enum implemented in my code at the moment. Check if you didn't type the wrong names (@Convert for the mapped attribute and @Converter for the class).Emulsify
When using SpringData, you need to remove the @Converter from the AbstractEnumConverter because Spring try automatically to register the converter into the Hibernate configuration, which does not work as the AbstractEnumConverter has no default noarg constructor. Otherwise, this works like a charm.Lumbricalis
Nice solution. I removed @Converter annotation from AbstractEnumConverter and @Convert(converter = IndOrientation.Converter.class) from IndOrientation field and instead of that I added @Converter(autoApply = true) to concrete implementation Converter.Nematode
This is the best solution I found in the whole net regarding enum-number mapping to db.Symbolics
I also implemented this solution but with @Tuom's variant because of a Persistence Unit error when deploying EAR on Jboss7.Tinny
Does-it work with EclipseLink (with payara for example) ?Lusitania
A
25

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:

Alpenstock answered 9/5, 2014 at 12:24 Comment(4)
But, enums can't extend another class? https://mcmap.net/q/373456/-how-to-extend-enum-class-from-abstract-classGaloshes
@pioto: That doesn't matter in this case. I've added an example how to do it.Alpenstock
Ahha! I thought you were talking about an abstract class for the Enum, not the Converter... this looks great, and I'll give it a try. Thanks!Galoshes
This works great, thanks, with one caveat: at least in Hibernate, you need to add implements AttributeConverter <Foo, String> to the declaration of your concrete FooConverter class, thanks to this bug: hibernate.atlassian.net/browse/HHH-8854Galoshes
I
10

This answer has been modified to take advantage of default interface methods in Java 8.

The number of components of the facility (enumerated below) remains at four, but the amount of required boilerplate is much less. The erstwhile AbstractEnumConverter class has been replaced by an interface named JpaEnumConverter which now extends the JPA AttributeConverter interface. Moreover, each placeholder JPA @Converter class now only requires the implementation of a single abstract method that returns the Class<E> object for the enum (for even less boilerplate).

This solution is similar to others and also makes use of the JPA Converter facility introduced in JPA 2.1. As generic types in Java 8 are not reified, there does not appear to be an easy way to avoid writing a separate placeholder 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. Encodable interface; the contract for an enum class that grants access to a String token for each enum constant. This is written only once and is implemented by all enum classes that are to be persisted via JPA. This interface also contains a static factory method for getting back the enum constant for its matching token.
  2. JpaEnumConverter interface; provides the common code for translating tokens to/from enum constants. This is also only written once and is implemented by all the placeholder @Converter classes in the project.
  3. Each Java enum class in the project implements the Encodable interface.
  4. Each JPA placeholder @Converter class implements the JpaEnumConverter interface.

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

public interface Encodable{

    String token();

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

The JpaEnumConverter interface is a generic interface that is also simple. It extends the JPA 2.1 AttributeConverter interface and implements its methods for translating back and forth between entity and database. These are then inherited by each of the JPA @Converter classes. The only abstract method that each placeholder class must implement, is the one that returns the Class<E> object for the enum.

public interface JpaEnumConverter<E extends Enum<E> & Encodable>
            extends AttributeConverter<E, String> {
    
    public abstract Class<E> getEnumClass();

    @Override
    public default String convertToDatabaseColumn(E attribute) {
        return (attribute == null)
            ? null
            : attribute.token();
    }

    @Override
    public default E convertToEntityAttribute(String dbData) {
        return (dbData == null)
            ? null
            : Encodeable.forToken(getEnumClass(), dbData);
    }
}

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 Encodable, and that the token for each enum constant is defined as a private field):

public enum GenderCode implements Encodable{
    
    MALE   ("M"), 
    FEMALE ("F"), 
    OTHER  ("O");
    
    final String e_token;

    GenderCode(String v) {
        this.e_token = v;
    }

    @Override
    public String token() {    // the only abstract method of Encodable
        return this.e_token;
    }
}

The boilerplate for every placeholder JPA 2.1 @Converter class would now look like the code below. Note that every such converter will need to implement JpaEnumConverter and provide the implementation for getEnumClass() ... and that's all! The implementations for the JPA AttributeConverter interface methods are inherited.

@Converter
public class GenderCodeConverter 
                 implements JpaEnumConverter<GenderCode> {

    @Override
    public Class<GenderCode> getEnumClass() {    // sole abstract method
        return GenderCode.class;  
    }
} 

These placeholder @Converter classes can be readily nested as static member classes of their associated enum classes.

Isomerism answered 5/6, 2016 at 22:41 Comment(0)
M
0

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,
        ...
   );
Merciful answered 13/8, 2020 at 18:46 Comment(2)
I don't understand, if getConverterClass is totally useless for what you have implemented, it doesn't make any sense in this code!Anti
The function is never called, but by having it in the interface, it forces the implementer to provide the specific companion converter class. JPA should provide a 1 shot converter class that always does the conversion when the enum implements a specific interface, but as far as I know it doesn't. So that is why you have to provide the companion converter class, over and over again, for each PersistedEnum specific implementation.Merciful
W
0

I found a way to do this without using java.lang.Class, default methods or reflection. I did this by using a Function that is passed to the Convertor in the constructor from the enum, using method reference. Also, the Convertos from the enum should be private, no need for them outside.

  1. Interface that Enums should implement in order to be persisted
public interface PersistableEnum<T> {
            
 /** A mapping from an enum value to a type T (usually a String, Integer etc).*/
 T getCode();
            
}
  1. The abstract converter will use a Function in order to cover convertToEntityAttribute transformation
@Converter
public abstract class AbstractEnumConverter<E extends Enum<E> & PersistableEnum<T>, T> implements AttributeConverter<E, T> {

 private Function<T, E> fromCodeToEnum;

 protected AbstractEnumConverter(Function<T, E> fromCodeToEnum) {
   this.fromCodeToEnum = fromCodeToEnum;
 }

 @Override
 public T convertToDatabaseColumn(E persistableEnum) {
   return persistableEnum == null ? null : persistableEnum.getCode();
 }

 @Override
 public E convertToEntityAttribute(T code) {
   return code == null ? null : fromCodeToEnum.apply(code);
 }

}
  1. The enum will implement the interface (I am using lombok for the getter) and create the converted by using a constructor that receives a Function, I pass the ofCode using method reference. I prefer this instead of working with java.lang.Class or using reflection, I have more freedom in the enums.
@Getter 
public enum CarType implements PersistableEnum<String> {

    DACIA("dacia"),
    FORD("ford"),
    BMW("bmw");
    
    public static CarType ofCode(String code) {
      return Arrays.stream(values())
                .filter(carType -> carType.code.equalsIgnoreCase(code))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Invalid car type code."));
      }
    
    private final String code;
    
    CarType(String code) {
      this.code = code;
    }
       
    @Converter(autoApply = true)
    private static class CarTypeConverter extends AbstractEnumConverter<CarType, String> {
      protected CarTypeConverter () {
        super(CarType::ofCode);
      }
    }    
}

4.In the entity you just have to use the enum type and it will save it's String code.

  @Column(name = "CAR_TYPE")
  private CarType workflowType;
Wholly answered 12/10, 2021 at 12:42 Comment(0)
A
0

If you don't mind reflection, this works. Credit to another SO answer inline.

abstract class EnumTypeConverter<EnumType,ValueType> implements AttributeConverter<EnumType, ValueType> {

    private EnumType[] values

    @Override
    ValueType convertToDatabaseColumn(EnumType enumInstance) {
        return enumInstance ? enumInstance.getProperty(getValueColumnName()) : null
    }

    @Override
    EnumType convertToEntityAttribute(ValueType dbData) {

        if(dbData == null){
            return null
        }

        EnumType[] values = getValues()
        EnumType rtn = values.find {
            it.getProperty(getValueColumnName()).equals(dbData)
        }
        if(!rtn) {
            throw new IllegalArgumentException("Unknown ${values.first().class.name} value: ${dbData}")
        }
        rtn
    }

    private EnumType[] getValues() {
        if(values == null){
            Class cls = getTypeParameterType(getClass(), EnumTypeConverter.class, 0)
            Method m = cls.getMethod("values")
            values = m.invoke(null) as EnumType[]
        }
        values
    }

    abstract String getValueColumnName()

    // https://mcmap.net/q/54715/-get-generic-type-of-class-at-runtime
    private static Class<?> getTypeParameterType(Class<?> subClass, Class<?> superClass, int typeParameterIndex) {
        return getTypeVariableType(subClass, superClass.getTypeParameters()[typeParameterIndex])
    }

    private static Class<?> getTypeVariableType(Class<?> subClass, TypeVariable<?> typeVariable) {
        Map<TypeVariable<?>, Type> subMap = new HashMap<>()
        Class<?> superClass
        while ((superClass = subClass.getSuperclass()) != null) {

            Map<TypeVariable<?>, Type> superMap = new HashMap<>()
            Type superGeneric = subClass.getGenericSuperclass()
            if (superGeneric instanceof ParameterizedType) {

                TypeVariable<?>[] typeParams = superClass.getTypeParameters()
                Type[] actualTypeArgs = ((ParameterizedType) superGeneric).getActualTypeArguments()

                for (int i = 0; i < typeParams.length; i++) {
                    Type actualType = actualTypeArgs[i]
                    if (actualType instanceof TypeVariable) {
                        actualType = subMap.get(actualType)
                    }
                    if (typeVariable == typeParams[i]) return (Class<?>) actualType
                    superMap.put(typeParams[i], actualType)
                }
            }
            subClass = superClass
            subMap = superMap
        }
        return null
    }
}

Then in the entity class:

enum Type {
        ATYPE("A"), ANOTHER_TYPE("B")
        final String name

        private Type(String nm) {
            name = nm
        }
    }

...

@Column
Type type

...

@Converter(autoApply = true)
    static class TypeConverter extends EnumTypeConverter<Type,String> {
        String getValueColumnName(){
            "name"
        }
}

This is written in groovy, so you'll need some adjustments for Java.

Arcanum answered 28/1, 2022 at 1:39 Comment(0)
G
0

For those working in Kotlin, here is an example of an abstract converter:

enum class MyEnum(override val serializedAs: Int) : SerializableEnum {
    A(0),
    B(1),
    C(2),
}

@Converter(autoApply = true)
class MyEnumConverter : AbstractEnumConverter<MyEnum>(MyEnum::class)
interface SerializableEnum {
    val serializedAs: Int
}

abstract class AbstractEnumConverter<TEnum>(enumType: KClass<TEnum>) : AttributeConverter<TEnum, Int> where TEnum : SerializableEnum {

    var fromSerialized = enumType.java.enumConstants.associateBy { it.serializedAs }

    init {
        if (fromSerialized.size != enumType.java.enumConstants.size) {
            throw IllegalStateException("Serializable enum $enumType must have unique `serializedAs` values.")
        }
    }

    override fun convertToDatabaseColumn(enum: TEnum?) = enum?.serializedAs

    override fun convertToEntityAttribute(enum: Int?) = enum?.let { fromSerialized[it] }
}

Ganiats answered 2/3, 2023 at 8:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.