Hibernate 6 cannot persist Enum as Ordinal in VARCHAR column

After updating to Hibernate 6.2.4 I started seeing ArrayIndexOutOfBounds exceptions when Hibernate tries to map from table to entity.

java.lang.ArrayIndexOutOfBoundsException: Index 48 out of bounds for length 6
	at org.hibernate.type.descriptor.java.EnumJavaType.fromInteger(EnumJavaType.java:194)
			62 lines skipped for [org.springframework, org.hibernate]

I eventually figured out that it was because Hibernate had started interpreting the values from the DB differently. For example, a “0” stored in a VARCHAR column would be treated as 48 (char/ASCII value), and that number would then be used as the ordinal value when converting it to an Enum.

I was able to fix the issue by changing the table column type to INT, but this does still seem like a bug anyway.

Unfortunately, I’ve never been able to gain access to the Hibernate Issue tracker for some reason so I’m posting about it here instead.

Did you read the migration guides?

Yes, but I don’t see anything relevant to my issue in them. There are some changes relating to TINYINT/SMALLINT datatypes for enums, but that’s not what I’m dealing with.

My issue is that 5.x treats a VARCHAR “0” as 0, but 6.x treats a VARCHAR “0” as 48.

I had the same problem (on Hibernate 6.2.4 and MySQL), as @Lars_Benedetto is saying Hibernate now reads a VARCHAR column database column mapping an enum field as if it was an integer. But it does it by trying to convert the byte value of "0" to an int, instead of doing something like Integer.parseInt(...)

In my case the issue was that someone (it was me…) forgot to add @Enumerated(EnumType.STRING) and Hibernate defaulted (as it should) to an ordinal mapping.
On 5.x it was fine storing the ordinal as a VARCHAR but now that breaks in 6.x

I’m not sure if that scenario is supported or maybe it was not supposed to work in 5.x

On MySQL, enums are now stored using the ENUM datatype by default

This is the relevant part. On MySQL the native enum type is used now if the configuration for a mapping resolves to EnumType.STRING, so you need to migrate your schema. I think it should be possible to fallback to the old representation by annotating @JdbcTypeCode(SqlTypes.VARCHAR)

1 Like

You are absolutely right. Please add this feature.
Until then, we have no other option than to completely disable Hibernate validation.

Carrying out a complete database migration just to change an enum type is unthinkable for us.

It’s just a few alter table statements I guess so not sure why you think it’s a big deal to migrate. Either way, you can also just annotate @JdbcTypeCode(SqlTypes.VARCHAR) on the enum attributes if you really want the columns to stay of type varchar.

It here a was to avoid creating ENUM column in MySQL and stick to VARCHAR as it was done before?

  1. Tried with @Column(columnDefinition = "VARCHAR(16)"), but it won’t work while @Enumerated(EnumType.STRING) is present.
  2. Tried to remove @Enumerated(EnumType.STRING) and that rasied exception at runtime.

I don’t want to use INT, TINYINT, BIGINT, any of int variants because I want it to be independent of enum items order in Java and to be able to add new enum element without writing database update code.

public enum Status {
	NEW, PROCESSING, INCOMPLETE, REJECTED, DONE;
}

@Enumerated(EnumType.STRING)
private Status status;

Like I wrote before, just annotate @JdbcTypeCode(SqlTypes.VARCHAR).

1 Like

Sorry! Missed that one! My bad!

It’s just a few alter table statements I guess so not sure why you think it’s a big deal to migrate.

Well, I tried to migrate but these tables are extremely large and mission critical. Altering them causes table locks making the service unavailable for several hours.

I’ve also tried JdbcTypeCode(SqlTypes.VARCHAR) but it is not working for me. I get

Caused by: org.hibernate.HibernateException: Unknown wrap conversion requested: java.lang.Integer to java.lang.String : `org.hibernate.type.descriptor.java.StringJavaType` (java.lang.String)
	at org.hibernate.type.descriptor.java.JavaTypeHelper.unknownWrap(JavaTypeHelper.java:24)
	at org.hibernate.type.descriptor.java.AbstractClassJavaType.unknownWrap(AbstractClassJavaType.java:116)
	at org.hibernate.type.descriptor.java.StringJavaType.wrap(StringJavaType.java:103)
	at org.hibernate.type.descriptor.java.StringJavaType.wrap(StringJavaType.java:27)
	at org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter.toRelationalValue(OrdinalEnumValueConverter.java:45)
	at org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter.toRelationalValue(OrdinalEnumValueConverter.java:23)

So what can I do to restore the old behavior where “0” is interpreted as 0 instead of 48?

I’d prefer not to have to map the column as a regular String and then do all the conversions manually.

If you want an ordinal mapping, you should be able to simply annotate @Enumerated(EnumType.ORDINAL)

Yes, it’s been annotated with that the whole time

@Enumerated(EnumType.ORDINAL)
@Column(name = "token_type", nullable = false)
private TokenType tokenType;
`token_type` VARCHAR(255) NOT NULL

This model makes no sense. Why do you store enum ordinals into a varchar column? If the JDBC driver supports reading/writing integers into varchar columns, then I guess your mapping should work, but this is definitely not something that ORM supports.

The model is stupid and it sucks, but it is legacy code and it cannot be changed (I tried, but the table is too large).

This worked in previous versions of Hibernate, it is only after updating that this stopped working.

Please create a test case(https://github.com/hibernate/hibernate-test-case-templates/blob/master/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/JPAUnitTestCase.java) that reproduces your issue, and also try showing how it works with ORM 5(https://github.com/hibernate/hibernate-test-case-templates/blob/master/orm/hibernate-orm-5/src/test/java/org/hibernate/bugs/JPAUnitTestCase.java). You can attach the reproducers on a newly created JIRA issue(https://hibernate.atlassian.net).

Ok, I have created test cases which reproduce the issue.

https://hibernate.atlassian.net/browse/HHH-17017

The test passes in Hibernate 5 and fails in Hibernate 6.

Also, for some reason, the test passes with the h2 database so I had to use a MySQL database via docker-compose.

@Lars_Benedetto can you try to add an AttributeConverter and use it to convert your TokenType?

Remove the annotation @Enumarted in TokenType.
You can either use annotation
@Convert(converter = TokenTypeConverter.class) or set the autoApply=true.

@Converter(autoApply = true)
public class TokenTypeConverter implements AttributeConverter<TokenType, String> {
    @Override
    public String convertToDatabaseColumn(TokenType tokenType){
        if (tokenType== null) {
            return null;
        }
        return tokenType.name();
    }

    @Override
    public TokenType convertToEntityAttribute(String ordinalAsString){
        if (ordinalAsString == null) {
            return null;
        }

        return Stream.of(TokenType.values())
                .filter(t -> ordinalAsString.equals(t.ordinal() + ""))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }
}

I think you meant to return tokenType.ordinal() + ""; in convertToDatabaseColumn().

With that change, your suggestion is a valid workaround. The actual workaround I’m using is similar.

@JdbcTypeCode(SqlTypes.VARCHAR)

This doesn’t work :frowning:

The hibernate validation fails if the column is varchar, it still expects enum.

I also tried without success:

@Column(columnDefinition = "varchar(255)")

@JdbcType(VarcharJdbcType.class)