hypersistence-utils icon indicating copy to clipboard operation
hypersistence-utils copied to clipboard

Java Enum to PostgreSQL Enum Type lowecase

Open asyba opened this issue 4 years ago • 14 comments
trafficstars

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 avatar Jan 08 '21 21:01 asyba

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

vladmihalcea avatar Jan 08 '21 21:01 vladmihalcea

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

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 avatar Jan 11 '21 12:01 asyba

@asyba I can investigate it via consulting if you're interested.

vladmihalcea avatar Jan 11 '21 12:01 vladmihalcea

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

asyba avatar Jan 12 '21 19:01 asyba

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.

vladmihalcea avatar Jan 12 '21 20:01 vladmihalcea

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

hinrik avatar Jun 02 '22 12:06 hinrik

Looks like a good implementation. I'll add it to the current Enum types.

vladmihalcea avatar Jun 02 '22 14:06 vladmihalcea

Still open?

igr avatar Nov 09 '22 23:11 igr

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.

vladmihalcea avatar Nov 10 '22 02:11 vladmihalcea

I can make a PR for it.

hinrik avatar Nov 10 '22 13:11 hinrik

@hinrik Thanks. Looking forward to it.

vladmihalcea avatar Nov 10 '22 16:11 vladmihalcea

Is this issue still open? i face the same issue.

dabdirb avatar Oct 07 '23 01:10 dabdirb

@dabdirb Looking forward to your Pull Request that fixes the issue.

vladmihalcea avatar Oct 07 '23 02:10 vladmihalcea

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

  1. use AttributeConverter on 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.
  2. create child class that inherit from PostgreSQLEnumType, replace NamedEnumValueConverter to a self defined converter class, maybe named as StringEnumValueConverter, to use enum.toString() to get the string representation of a enum class.

dabdirb avatar Oct 15 '23 07:10 dabdirb