问题
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!
回答1:
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
回答2:
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.
回答3:
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:
Encodeable
interface; the contract for an enum class that grants access to aString
token for each enum constant (also a factory for getting the enum constant for a matching token)AbstractEnumConverter
class; provides the common code for translating tokens to/from enum constants- Java enum classes that implement the
Encodeable
interface - 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);
}
}
来源:https://stackoverflow.com/questions/23564506/is-it-possible-to-write-a-generic-enum-converter-for-jpa