Hibernate 6 migration: custom composite user type not working

I am migrating a project from Hibernate 5 to Hibernate 6. One old class implemented UserType interface in Hibernate 5 but cannot be compiled in Hibernate 6. So, I need to create new custom user type mapping for the class below:

public class AlertData implements Serializable {

  private static final long serialVersionUID = 1L;

  private AlertType type;

  private Object data;

  private AlertData(AlertType type, Object data) {
    this.type = type;
    this.data = data;
  }
}
  public enum AlertType {
    PRODUCER, TIME, SIZE;
  }

The “type” should be mapped to a String. The “data” should be saved as a Jason String in database. Code below is my old class:

public class AlertDataType implements UserType {

  private ObjectMapper mapper = new ObjectMapper();

  @Override
  public int[] sqlTypes() {
    return new int[] {StringType.INSTANCE.sqlType(), Types.JAVA_OBJECT};
  }

  @Override
  public Class returnedClass() {
    return AlertData.class;
  }

  @Override
  public boolean equals(Object x, Object y) throws HibernateException {
    if (x == y)
      return true;
    if (Objects.isNull(x) || Objects.isNull(y))
      return false;
    return x.equals(y);
  }

  @Override
  public int hashCode(Object o) throws HibernateException {
    return o.hashCode();
  }

  @Override
  public Object nullSafeGet(ResultSet resultSet, String[] names,
      SharedSessionContractImplementor sharedSessionContractImplementor, Object o)
      throws HibernateException, SQLException {
    AlertType type = AlertType.valueOf(resultSet.getString(names[0]));
    String jsonData = resultSet.getString(names[1]);

    // remove leading & ending double-quote (")
    if (jsonData.length() > 2 && jsonData.startsWith("\"") && jsonData.endsWith("\"")) {
      jsonData = jsonData.substring(1, jsonData.length() - 1);
    }
    // remove \ character.
    jsonData = jsonData.replace("\\", "");
    Object data = null;
    if (type == AlertType.SIZE) {
      try {
        data = mapper.readValue(jsonData, SizeAlertData.class);
      } catch (JsonProcessingException e) {
        throw new RuntimeException("Failed to deserialize Size Alert Data: " + e.getMessage(), e);
      }
    } else if (type == AlertType.TIME) {
      try {
        data = mapper.readValue(jsonData, TimeAlertData[].class);
      } catch (JsonProcessingException e) {
        throw new RuntimeException("Failed to deserialize Timed Alert Data: " + e.getMessage(), e);
      }
    } else if (type == AlertType.PRODUCER) {
      try {
        data = mapper.readValue(jsonData, String[].class);
      } catch (JsonProcessingException e) {
        throw new RuntimeException("Failed to deserialize Producer Alert Data: " + e.getMessage(), e);
      }
    }
    return AlertData.create(type, data);
  }

  @Override
  public void nullSafeSet(PreparedStatement preparedStatement, Object o, int i,
      SharedSessionContractImplementor sharedSessionContractImplementor)
      throws HibernateException, SQLException {
    if (o == null) {
      preparedStatement.setNull(i, StringType.INSTANCE.sqlType());
      preparedStatement.setNull(i + 1, Types.OTHER);
    } else {
      final AlertData alertData = (AlertData) o;
      preparedStatement.setString(i, alertData.getType().toString());
      try {
        String s = mapper.writeValueAsString(alertData.getData());
        preparedStatement.setObject(i + 1, s, Types.OTHER);
      } catch (JsonProcessingException e) {
        throw new RuntimeException("Failed to serialize alert data: " + e.getMessage(), e);
      }
    }
  }

  @Override
  public Object deepCopy(Object o) throws HibernateException {
    if (Objects.isNull(o)) {
      return null;
    }
    AlertData alertData = (AlertData) o;
    Object data = null;
    if (alertData.getType() == AlertType.SIZE) {
      SizeAlertData sizeAlertData = (SizeAlertData) alertData.getData();
      data = SizeAlertData.create(sizeAlertData.min, sizeAlertData.max);
    } else if (alertData.getType() == AlertType.TIME) {
      TimeAlertData[] timeAlertData = (TimeAlertData[]) alertData.getData();
      data = Arrays.copyOf(timeAlertData, timeAlertData.length);
    } else if (alertData.getType() == AlertType.PRODUCER) {
      String[] producers = (String[]) alertData.getData();
      data = Arrays.copyOf(producers, producers.length);
    }
    return AlertData.create(alertData.getType(), data);
  }

  @Override
  public boolean isMutable() {
    return false;
  }

  @Override
  public Serializable disassemble(Object o) throws HibernateException {
    return (Serializable) o;
  }

  @Override
  public Object assemble(Serializable serializable, Object o) throws HibernateException {
    return serializable;
  }

  @Override
  public Object replace(Object o, Object o1, Object o2) throws HibernateException {
    return o;
  }
}

I created a class implements CompositeUserType interface but got exception “java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Enum”.

My new custom user type class:

public class AlertDataType implements CompositeUserType<AlertData> {

  private ObjectMapper mapper = new ObjectMapper();

  public static class AlertDataMapper {
    private Object data;
    private AlertType type;
  }

  @Override
  public Object getPropertyValue(AlertData component, int property) throws HibernateException {
    if (component == null) {
      return null;
    }
    switch (property) {
      case 0:
        if (component.getData() == null) {
          return null;
        }
        try {
          return mapper.writeValueAsString(component.getData());
        } catch (JsonProcessingException e) {
          throw new HibernateException("Failed to serialize alert data", e);
        }
      case 1:
        return component.getType() == null ? null : component.getType().toString();
      default:
        throw new HibernateException("Unknown property index: " + property);
    }
  }


  @Override
  public AlertData instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
    // Ensure proper handling of AlertType conversion
    String typeString = values.getValue(1, String.class);
    AlertType type;
    try {
      type = AlertType.valueOf(typeString);
    } catch (IllegalArgumentException e) {
      throw new HibernateException(
          "Invalid AlertType value in database: " + values.getValue(0, String.class), e);
    }

    String jsonData = values.getValue(0, String.class);
    // remove leading & ending double-quote (")
    if (jsonData.length() > 2 && jsonData.startsWith("\"") && jsonData.endsWith("\"")) {
      jsonData = jsonData.substring(1, jsonData.length() - 1);
    }
    // remove \ character.
    jsonData = jsonData.replace("\\", "");
    Object data = null;

    try {
      switch (type) {
        case SIZE:
          data = mapper.readValue(jsonData, SizeAlertData.class);
          break;
        case TIME:
          data = mapper.readValue(jsonData, TimeAlertData[].class);
          break;
        case PRODUCER:
          data = mapper.readValue(jsonData, String[].class);
          break;
        default:
          throw new HibernateException("Unhandled AlertType: " + type);
      }
    } catch (JsonProcessingException e) {
      throw new HibernateException(
          "Failed to deserialize Alert Data for type " + type + ": " + e.getMessage(), e);
    }

    return AlertData.create(type, data);
  }


  @Override
  public Class<?> embeddable() {
    return AlertDataMapper.class;
  }

  @Override
  public Class<AlertData> returnedClass() {
    return AlertData.class;
  }

  @Override
  public boolean equals(AlertData x, AlertData y) {
    if (x == y)
      return true;
    if (x == null || y == null)
      return false;
    return x.equals(y);
  }

  @Override
  public int hashCode(AlertData x) {
    return x.hashCode();
  }

  @Override
  public AlertData deepCopy(AlertData value) {
    if (Objects.isNull(value)) {
      return null;
    }
    AlertData alertData = value;
    Object data = null;
    if (alertData.getType() == AlertType.SIZE) {
      SizeAlertData sizeAlertData = (SizeAlertData) alertData.getData();
      data = SizeAlertData.create(sizeAlertData.min, sizeAlertData.max);
    } else if (alertData.getType() == AlertType.TIME) {
      TimeAlertData[] timeAlertData = (TimeAlertData[]) alertData.getData();
      data = Arrays.copyOf(timeAlertData, timeAlertData.length);
    } else if (alertData.getType() == AlertType.PRODUCER) {
      String[] producers = (String[]) alertData.getData();
      data = Arrays.copyOf(producers, producers.length);
    }
    return AlertData.create(alertData.getType(), data);
  }

  @Override
  public boolean isMutable() {
    return false;
  }

  @Override
  public Serializable disassemble(AlertData value) {
    return value;
  }

  @Override
  public AlertData assemble(Serializable cached, Object owner) {
    return (AlertData) cached;
  }

  @Override
  public AlertData replace(AlertData original, AlertData target, Object owner) {
    return original;
  }


}

Changes in entity class:

  @Embedded
  @AttributeOverride(name = "data", column = @Column(name = "data", nullable = false))
  @AttributeOverride(name = "type", column = @Column(name = "type", nullable = false))
  @CompositeType(AlertDataType.class)
  protected AlertData data;

Exception stack trace:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Enum (java.lang.String and java.lang.Enum are in module java.base of loader 'bootstrap')
    at org.hibernate.type.EnumType.equals(EnumType.java:72)
    at org.hibernate.type.CustomType.isEqual(CustomType.java:166)
    at org.hibernate.type.AbstractType.isSame(AbstractType.java:103)
    at org.hibernate.type.AbstractType.isDirty(AbstractType.java:87)
    at org.hibernate.type.ComponentType.isDirty(ComponentType.java:279)
    at org.hibernate.persister.entity.DirtyHelper.isDirty(DirtyHelper.java:69)
    at org.hibernate.persister.entity.DirtyHelper.findDirty(DirtyHelper.java:44)
    at org.hibernate.persister.entity.AbstractEntityPersister.findDirty(AbstractEntityPersister.java:4525)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.dirtyCheck(DefaultFlushEntityEventListener.java:555)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.isUpdateNecessary(DefaultFlushEntityEventListener.java:211)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:135)
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:214)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:90)
    at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:48)
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
    at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1385)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:100)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:305)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:246)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:546)
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:363)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.list(QuerySqmImpl.java:1032)
    at org.hibernate.query.Query.getResultList(Query.java:94)
    at com.missionessential.dmeapp.db.jpa.AlertJpaRepository.getByTimeRange(AlertJpaRepository.java:64)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:218)
    at jdk.proxy2/jdk.proxy2.$Proxy95.getByTimeRange(Unknown Source)
    at com.missionessential.dmeapp.db.jpa.AlertsTest.persistSize(AlertsTest.java:78)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
    at org.junit.vintage.engine.execution.RunnerExecutor.execute(RunnerExecutor.java:42)
    at org.junit.vintage.engine.VintageTestEngine.executeAllChildren(VintageTestEngine.java:80)
    at org.junit.vintage.engine.VintageTestEngine.execute(VintageTestEngine.java:72)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
    at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

Should I use CompositeUserType in this scenario? How to fix this issue?

The problem seems to be in your implementation. You are wrongly using toString() in getPropertyValue:

        return component.getType() == null ? null : component.getType().toString();

Your AlertDataMapper class should map the relational view of the type. So IMO it should look like the following instead:

  public static class AlertDataMapper {
    @JdbcTypeCode(SqlTypes.JSON)
    private String data;
    private AlertType type;
  }
1 Like

Thanks for your reply. I will revise my implementation.