Explicit jdbc type set by AttributeBinder not respected by Hibernate

In my AttributeBinder#bind implementation I end with something like this:

    val basicValue = (BasicValue)property.getValue();
    val jdbcType = getJdbcType(enumToBit);

    basicValue.setExplicitJavaTypeAccess(typeConfiguration ->
        new EnumSetJavaType(enumType, enumToBit, property.isOptional()));
    basicValue.setExplicitJdbcTypeCode(jdbcType);
    basicValue.setExplicitJdbcTypeAccess(typeConfiguration ->
        typeConfiguration.getJdbcTypeRegistry().getDescriptor(jdbcType));

jdbcType is always some sort of numeric type. In most cases it is TINYINT (-6). But Hibernate ignores this type and always tries to read the value using ARRAY (2003), which fails.

Additionally, the java type I set, returns the same numeric type but BasicJavaType#getRecommendedJdbcType never seems to get invoked anyway.

What am I missing here? Why does Hibernate not respect the jdbc type I tell it to use?

There is so much context missing in this question, it’s impossible to answer.

  • What Hibernate ORM version are you using?
  • How does your entity model look like?
  • What code are you referring to here?
  • Do you get an exception? If so, share it.
  • What query are you executing that triggers this behavior?

I’m using Hibernate 6.6.0.

The entity model:

@Entity
@Immutable
@Table(name = "reason")
public class Reason implements Serializable
{
  @Id @Column(name = "reasonid", unique = true, insertable = false, updatable = false, nullable = false)
  protected int reasonid;

  @Column(name = "reasonnumber", insertable = false, updatable = false, nullable = false)
  protected @NotNull String reasonnumber;

  @Column(name = "text", insertable = false, updatable = false, nullable = false)
  protected @NotNull String text;

  @Column(name = "isactive", insertable = false, updatable = false, nullable = false)
  protected boolean active;

  @Column(name = "valid_from", insertable = false, updatable = false, nullable = false)
  protected @NotNull LocalDate validFrom;

  @Column(name = "valid_to", insertable = false, updatable = false, nullable = false)
  protected @NotNull LocalDate validTo;

  @Column(name = "special_feature", nullable = false, updatable = false, insertable = false)
  @EnumBitmaskMap({
      @Bitmask(bit = 0, name = "Ergebnis_LE")
  })
  protected @NotNull Set<ReasonSpecialFeature> specialFeatures;
}

What Hibernate tries to do:

2024-10-21 16:04:52,407|DEBUG|50000000000015|196C3B74#2|317|JdbcSelectExecutorStandardImpl.java:318|org.hibernate.orm.sql.exec - Skipping reading Query result cache data: cache-enabled = false, cache-mode = IGNORE
2024-10-21 16:04:52,408|DEBUG|50000000000015|196C3B74#2|317|SqlStatementLogger.java:135|org.hibernate.SQL - 
    select
        r1_0.reasonid,
        r1_0.isactive,
        r1_0.reasonnumber,
        r1_0.special_feature,
        r1_0.text,
        r1_0.valid_from,
        r1_0.valid_to 
    from
        ismed_dev.reason r1_0 
    where
        r1_0.reasonid=?
2024-10-21 16:04:52,476|DEBUG|50000000000015|196C3B74#2|317|NavigablePathMapToInitializer.java:76|org.hibernate.orm.results - Initializer list:
	  de.mdk.ismed.common.persistence.entity.ismed_dev.Reason -> EntityJoinedFetchInitializer(de.mdk.ismed.common.persistence.entity.ismed_dev.Reason)@1359859178 (SingleTableEntityPersister(de.mdk.ismed.common.persistence.entity.ismed_dev.Reason))

The exception:

2024-10-21 16:04:52,476|DEBUG|50000000000015|196C3B74#2|317|SqlExceptionHelper.java:134|org.hibernate.engine.jdbc.spi.SqlExceptionHelper - Could not extract column [4] from JDBC ResultSet [n/a]
java.sql.SQLException: UngĂĽltiger Spaltentyp: getARRAY not implemented for class oracle.jdbc.driver.T4CNumberAccessor
	at oracle.jdbc.driver.GeneratedAccessor.getARRAY(GeneratedAccessor.java:906) ~[ojdbc8.jar:19.3.0.0.0]
	at oracle.jdbc.driver.GeneratedStatement.getARRAY(GeneratedStatement.java:343) ~[ojdbc8.jar:19.3.0.0.0]
	at oracle.jdbc.driver.GeneratedScrollableResultSet.getARRAY(GeneratedScrollableResultSet.java:466) ~[ojdbc8.jar:19.3.0.0.0]
	at oracle.jdbc.driver.GeneratedScrollableResultSet.getArray(GeneratedScrollableResultSet.java:87) ~[ojdbc8.jar:19.3.0.0.0]
	at weblogic.jdbc.wrapper.ResultSet_oracle_jdbc_driver_ForwardOnlyResultSet.getArray(Unknown Source) ~[CodeGenerator.class:?]
	at org.hibernate.dialect.OracleArrayJdbcType$2.doExtract(OracleArrayJdbcType.java:131) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.type.descriptor.jdbc.BasicExtractor.extract(BasicExtractor.java:44) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.getCurrentRowValue(JdbcValuesResultSetImpl.java:387) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.getJdbcValue(RowProcessingStateStandardImpl.java:152) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.basic.BasicResultAssembler.extractRawValue(BasicResultAssembler.java:54) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.basic.BasicResultAssembler.assemble(BasicResultAssembler.java:60) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.extractConcreteTypeStateValues(EntityInitializerImpl.java:1492) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.initializeEntityInstance(EntityInitializerImpl.java:1229) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.initializeInstance(EntityInitializerImpl.java:1208) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.initializeInstance(EntityInitializerImpl.java:94) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:244) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:141) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.spi.ListResultsConsumer.readUniqueAssert(ListResultsConsumer.java:262) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:198) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:35) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:224) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:145) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:117) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.loader.ast.internal.SingleIdEntityLoaderStandardImpl.load(SingleIdEntityLoaderStandardImpl.java:74) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:3777) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3766) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:604) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.loadFromCacheOrDatasource(DefaultLoadEventListener.java:590) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:560) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:544) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:206) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.loadWithRegularProxy(DefaultLoadEventListener.java:289) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:241) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:110) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:69) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1229) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1075) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializer.initialize(EntitySelectFetchInitializer.java:236) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializer.resolveInstance(EntitySelectFetchInitializer.java:153) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializer.resolveInstance(EntitySelectFetchInitializer.java:44) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:239) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:141) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.internal.ScrollableResultsImpl.prepareCurrentRow(ScrollableResultsImpl.java:133) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.internal.ScrollableResultsImpl.next(ScrollableResultsImpl.java:52) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at org.hibernate.query.internal.ScrollableResultsIterator.hasNext(ScrollableResultsIterator.java:33) ~[hibernate-core-6.6.0.Final.jar:6.6.0.Final]
	at java.util.Iterator.forEachRemaining(Iterator.java:132) ~[?:?]
	at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801) ~[?:?]
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[?:?]
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[?:?]
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[?:?]
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?]
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[?:?]
	at de.mdk.ismed.gui.service.valuelist.ValueListLoaderService.load(ValueListLoaderService.java:155) ~[ismed3-gui-services.jar:6.0]

From the stack trace you can see that Hibernate associated OracleArrayJdbcType with the attribute I explicitly binded to something else. The bind method of the AttributeBinder is executed but the explicit type accessor settings on the BasicValue are ignored.

I have found the problem and it seems to be a recurring issue in Hibernate 6.

AttributeBinder#bind allows me to customize the attribute binding but doesn’t provide me with the information I need in order to do so. In this case (and many more cases for various customized bindings/types) the 1st thing I need is the java.lang.reflect.Type (more specifically, the java.lang.reflect.ParameterizedType) of the property It almost seems like there has been put a lot of work into hiding this essential information.

Now, in order to get the generic type of the property, I can only use resolve() and then getResolvedJavaType(). That’s great but at that point useless, because if I try to modify the explicit java/jdbc type acces or jdbc type code, it is not taken into account anymore because the Resolution object has been created already and it is only going to be resolved once.

Another possibility would be to directly access the contents of class member implicitJavaTypeAccess. Unfortunately it is not accessible in any way (other than resolve()) and would be the better solution, because I only need that value and nothing else and I would not have the problem of the one-time resolution creation.

For lack of a better alternative, I am now using a Byte-Buddy agent to extended class BasicValue with an interface, which implements a method to set the resolution to null, so it will be re-evaluated after I put in my changes.

I will probably change the agent to just provide me with the contents of implicitJavaTypeAccess as it is a better solution. Yet, it would be nice if BasicValue would just have this method, so I don’t have to resort to runtime byte code manipulation:

public Type getImplicitJavaType() {
  return implicitJavaTypeAccess.apply(getTypeConfiguration());
}

We like to keep things private and only expose things that are useful in a way that we want to expose them. If you build software that is used by thousands of other developers in ways that you don’t know, you will understand this defensive strategy that tries to minimize API surface that could cause breakage if it changes.

You have access to the Property in the binder, which provides access to the PropertyAccessStrategy through which you can get your hands on the reflective type as well.

I would suggest you also create a feature/improvement ticket in our issue tracker and explain why this is useful, then we can think about how to make the information accessible to you.

You didn’t share what this EnumBitmaskMap annotation does or what binder you’re using. Also, have you tried using a custom JavaType/JdbcType yet?

EnumBitmaskMap:

@Target(FIELD)
@Retention(RUNTIME)
@AttributeBinderType(binder = EnumBitmaskMapBinder.class)
public @interface EnumBitmaskMap
{
  Bitmask[] value() default {};

  @Target(ANNOTATION_TYPE)
  @Retention(RUNTIME)
  @interface Bitmask
  {
    int bit();
    String name();
  }
}

EnumBitmaskMapBinder:

public class EnumBitmaskMapBinder implements AttributeBinder<EnumBitmaskMap>
{
  @Override
  public void bind(EnumBitmaskMap annotation, MetadataBuildingContext buildingContext,
                   PersistentClass persistentClass, Property property)
  {
    val enumType = detectEnumType(property);
    val enumConstants = enumType.getEnumConstants();
    val enumToBit = new int[enumConstants.length];

    fill(enumToBit, -1);

    mapUsingBitmaskAnnotation(enumConstants, annotation, property, enumToBit);

    ((BasicValue)property.getValue()).setExplicitJavaTypeAccess(typeConfiguration ->
        new EnumSetJavaType(enumType, enumToBit, property.isOptional()));
  }


  private <T extends Enum<T>> Class<T> detectEnumType(@NotNull Property property)
  {
    val resolvableType = getImplicitJavaType(property);

    val setType = resolvableType.getRawClass();
    if (setType == null || !Set.class.isAssignableFrom(setType) || !setType.isAssignableFrom(EnumSet.class))
      throw new HibernateException("property " + propertyString(property) + " must be a Set or EnumSet");

    val elementType = resolvableType.getGeneric().getRawClass();
    if (elementType == null || elementType == Enum.class || !Enum.class.isAssignableFrom(elementType))
    {
      throw new HibernateException("generic parameter for property " + propertyString(property) +
          " must be an enumeration");
    }

    return (Class<T>)elementType;
  }


  private void mapUsingBitmaskAnnotation(@NotNull Enum<?>[] enumConstants, @NotNull EnumBitmaskMap annotation,
                                         @NotNull Property property, int @NotNull [] enumToBit)
  {
    val bitToEnumMap = new HashMap<Integer,Enum<?>>();
    val enumNameMap = Arrays
        .stream(enumConstants)
        .collect(toMap(Enum::name, identity()));

    for(val bitmask: annotation.value())
    {
      val name = bitmask.name();

      val enumValue = enumNameMap.get(name);
      if (enumValue == null)
      {
        throw new HibernateException("Enum name " + name + " for property " + propertyString(property) +
            " does not exist");
      }

      val bit = bitmask.bit();
      if (bit < 0 || bit > 63)
      {
        throw new HibernateException("Bit value associated with '" + name + "' for property " +
            propertyString(property) + " must be between 0 and 63");
      }

      if (bitToEnumMap.put(bit, enumValue) != null)
        throw new HibernateException("Duplicate definition for bit " + bit + " for enumeration value " + name);
    }

    bitToEnumMap.forEach((bit,enumValue) -> enumToBit[enumValue.ordinal()] = bit);
  }


  @RequiredArgsConstructor(access = PRIVATE)
  private static final class EnumSetJavaType<T extends Enum<T>> implements BasicJavaType<Set<T>>
  {
    private final Class<T> enumClass;
    private final int[] enumToBit;
    private final boolean nullable;

    @Override
    public Type getJavaType() {
      return forClassWithGenerics(Set.class, enumClass).getType();
    }

    @Override
    public Class<Set<T>> getJavaTypeClass() {
      return (Class)Set.class;
    }

    @Override
    public MutabilityPlan<Set<T>> getMutabilityPlan()
    {
      return new MutableMutabilityPlan<>() {
        @Override
        protected Set<T> deepCopyNotNull(Set<T> value) {
          return value.isEmpty() ? EnumSet.noneOf(enumClass) : EnumSet.copyOf(value);
        }
      };
    }

    @Override
    public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators)
    {
      val bits = Arrays.stream(enumToBit).max().orElse(0);

      if (bits < 8)
        return TinyIntJdbcType.INSTANCE;

      if (bits < 16)
        return SmallIntJdbcType.INSTANCE;

      if (bits < 32)
        return IntegerJdbcType.INSTANCE;

      return BigIntJdbcType.INSTANCE;
    }

    @Override
    public boolean useObjectEqualsHashCode() {
      return true;
    }

    @Override
    public String toString(Set<T> value) {
      return value == null ? "<null>" : value.stream().map(Enum::name).collect(joining(", "));
    }

    @Override
    public <X> X unwrap(Set<T> set, Class<X> type, WrapperOptions options)
    {
      if (Number.class.isAssignableFrom(type))
      {
        long bitmask = 0L;

        if (set == null)
        {
          if (nullable)
            return null;
        }
        else if (!set.isEmpty())
        {
          for(val enm: set)
          {
            val bit = enumToBit[enm.ordinal()];
            if (bit >= 0)
              bitmask |= 1L << bit;
          }
        }

        return (X)convertNumberToTargetClass(bitmask, (Class<? extends Number>)type);
      }

      throw new HibernateException("Could not convert '" + Set.class.getName() + "' to '" + type.getName() +
          "' using '" + getClass().getName() + "' to unwrap");
    }

    @Override
    public <X> EnumSet<T> wrap(X number, WrapperOptions options)
    {
      if (number == null)
        return nullable ? null : EnumSet.noneOf(enumClass);

      if (number instanceof Number)
      {
        val bitmask = ((Number)number).longValue();
        val set = EnumSet.noneOf(enumClass);

        if (bitmask != 0)
          for(val enm: enumClass.getEnumConstants())
          {
            val bit = enumToBit[enm.ordinal()];
            if (bit >= 0 && (bitmask & (1L << bit)) != 0)
              set.add(enm);
          }

        return set;
      }

      throw new HibernateException("Could not convert '" + number.getClass().getName() + "' to '" +
          EnumSet.class.getName() + "' using '" + getClass().getName() + "' to wrap");
    }
  }
}

In an entity I would use it like this:

  @Column(name = "feature", nullable = false)
  @EnumBitmaskMap({
      @Bitmask(bit = 0, name = "FEATURE_1"),
      @Bitmask(bit = 1, name = "FEATURE_2"),
      @Bitmask(bit = 2, name = "FEATURE_3"),
      @Bitmask(bit = 6, name = "FEATURE_4")
  })
  protected Set<Feature> features;

In EnumBitmaskMapBinder#detectEnumType there is a method invocation getImplicitJavaType which is the issue here. I have solved it using bytecode enhancement as described in my previous post but the field type is not accessible with methods/classes that are available. The PropertyAccessStrategy in Property is always null in the binder, so that is not going to help.

If there really is no other convenient way to get the generic type, I will create an improvement ticket.

I issued an improvement ticket.

1 Like