hypersistence-utils
hypersistence-utils copied to clipboard
Java Enum to PostgreSQL Enum Type lowecase
HI,
I have issue try to implement "Mapping a Java Enum to a database-specific Enumerated column type" from https://vladmihalcea.com/the-best-way-to-map-an-enum-type-with-jpa-and-hibernate/
my case is that in my PostgreSQL I have lowercase:
CREATE TYPE STATUS AS ENUM ('active', 'inactive');
No enum constant com.amcn.api.persistence.model.AccountState.active; nested exception is java.lang.IllegalArgumentException: No enum constant com.amcn.api.persistence.model.AccountState.active
org.springframework.dao.InvalidDataAccessApiUsageException: No enum constant com.amcn.api.persistence.model.AccountState.active; nested exception is java.lang.IllegalArgumentException: No enum constant com.amcn.api.persistence.model.AccountState.active
Caused by: java.lang.IllegalArgumentException: No enum constant com.amcn.api.persistence.model.AccountState.active
@asyba I think this can be fixed by providing a custom EnumJavaTypeDescriptor to the PostgreSQLEnumType.
If you implement it, send me a Pull Request so that I can review it. Looking forward to your solution.
@asyba I think this can be fixed by providing a custom
EnumJavaTypeDescriptorto thePostgreSQLEnumType.If you implement it, send me a Pull Request so that I can review it. Looking forward to your solution.
I tried using a @Converter
...
@Override
public String convertToDatabaseColumn(AccountState state) {
return state.toDbValue();
}
@Override
public AccountState convertToEntityAttribute(String dbData) {
return AccountState.from(dbData);
}
...
but using Converter tag is ignore when using Enumerated tag, do you know some way to allow both?
@Enumerated(EnumType.STRING)
@Type(type = "pgsql_enum")
// @Convert(converter = StatusConverter.class)
@Column(name = "EXAMPLE", nullable = false)
@asyba I can investigate it via consulting if you're interested.
@asyba I can investigate it via consulting if you're interested.
I found two solutions, I don't have time to make some PR. I can copy here:
1.:
public enum State {
active,
inactive;
}
(not following uppercase constant convection name)
+ PostgreSQLEnumType class with nullSafeSet
public enum State {
ACTIVE("active"),
INACTIVE("inactive");
private final String state;
State(String state) {
this.state = state;
}
public String geState() {
return state;
}
}
+ PostgreSQLEnumType class with nullSafeSet and nullSafeGet
public Object nullSafeGet(
ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws SQLException {
String label = rs.getString(names[0]);
if (rs.wasNull()) {
return null;
}
for (Object value : returnedClass().getEnumConstants()) {
if (value instanceof State) {
State labeledEnum = (State) value;
if (labeledEnum.geState().equals(label)) {
return value;
}
}
}
throw new IllegalStateException("Unknown " + returnedClass().getSimpleName() + " label");
}
The problem with this approach is that it hardcodes the enum value. The PostgreSQL type works with any Enum type you map on the entity.
I don't have time to make some PR.
I totally understand you since I don't have time for that either. Maybe one day someone will add support for it and then the framework will support this too.
At my company we have lower-case enum values in our DBs which we map to traditional Java enums with uppercase names.
For MySQL (might actually also work for other DBs, where an enum is transmitted to the DB as a string/VARCHAR):
public class LowerCaseMySQLEnumType extends LowerCaseEnumType {
@Override
@SneakyThrows
@SuppressWarnings("rawtypes")
public void nullSafeSet(
PreparedStatement statement,
Object value,
int index,
SharedSessionContractImplementor session
) {
if (value == null) {
statement.setNull(index, Types.VARCHAR);
} else {
statement.setString(index, ((Enum) value).name().toLowerCase());
}
}
}
For PostgreSQL:
public class LowerCasePostgreSQLEnumType extends LowerCaseEnumType {
@Override
@SneakyThrows
public void nullSafeSet(
PreparedStatement statement,
Object value,
int index,
SharedSessionContractImplementor session
) {
statement.setObject(index, value != null ? ((Enum<?>) value).name().toLowerCase() : null, Types.OTHER);
}
}
And the base class:
@SuppressWarnings("rawtypes")
public abstract class LowerCaseEnumType implements DynamicParameterizedType, UserType {
private Class<? extends Enum> enumClass;
@Override
public Object assemble(Serializable cached, Object owner) {
return cached;
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y;
}
@Override
public int hashCode(Object x) throws HibernateException {
return x == null ? 0 : x.hashCode();
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Object nullSafeGet(
ResultSet resultSet,
String[] names,
SharedSessionContractImplementor session,
Object owner
) throws HibernateException, SQLException {
var dbValue = resultSet.getString(names[0]);
if (resultSet.wasNull()) {
return null;
}
return createEnumInstance(dbValue.toUpperCase(), enumClass);
}
@SuppressWarnings("unchecked")
protected <T extends Enum<T>> T createEnumInstance(String name, Type type) {
return Enum.valueOf((Class<T>) type, name);
}
@Override
public abstract void nullSafeSet(
PreparedStatement statement,
Object value,
int index,
SharedSessionContractImplementor session
);
@Override
public Object replace(Object original, Object target, Object owner) {
return original;
}
@Override
public Class<? extends Enum> returnedClass() {
return enumClass;
}
@Override
public int[] sqlTypes() {
return new int[]{Types.VARCHAR};
}
@Override
@SuppressWarnings("unchecked")
public void setParameterValues(Properties parameters) {
var params = (ParameterType) parameters.get(PARAMETER_TYPE);
enumClass = params.getReturnedClass();
}
}
Looks like a good implementation. I'll add it to the current Enum types.
Still open?
Yes. That's the downside of sending a proposal in a comment rather than with a Pull Request.
But, you are free to submit the Pull Request if this is important to you. Otherwise, it's very likely that I'll forget about it.
I can make a PR for it.
@hinrik Thanks. Looking forward to it.
Is this issue still open? i face the same issue.
@dabdirb Looking forward to your Pull Request that fixes the issue.
@vladmihalcea I have identify the root cause of this issue, as you say, this issue caused by class PostgreSQLEnumType use new NamedEnumValueConverter to convert betweeen enum and string.
If i want to fix this issue, how can we get the string of a enum by a unified method?
My code define the method to get string is getValue() and above example define as public String geState()
unless we use name of enum :(
I reviewed hibernate doc, https://docs.jboss.org/hibernate/orm/6.3/userguide/html_single/Hibernate_User_Guide.html#basic-enums, which we add annotation @Enumerated(STRING), it mapped to enum name.
My conclusion is this issue can not be fixed by update this hibernate util lib, but still other way to workaround it.
- use
AttributeConverteron entity class enum field. this is simple and easy to implement, but if you can not let JPA auto create the postgres database table as enum type. - create child class that inherit from
PostgreSQLEnumType, replaceNamedEnumValueConverterto a self defined converter class, maybe named asStringEnumValueConverter, to use enum.toString() to get the string representation of a enum class.