Custom Composite User Type not working with hbm.xml in Hibernate 6.5

Hi everyone,
I have a CompositeUserType which defines two columns amount and currency as described in Vlad’s hypersistence-utils: MonetaryAmountType.java

The custom type works fine when used in an @ElementCollection when mapped with JPA annotations, as shown below:

@CompositeType(MonetaryAmountType.class)
@ElementCollection(fetch = FetchType.LAZY)
@OrderColumn(name = "amounts_idx")
@CollectionTable(name = "my_entity_monetary_amounts", joinColumns = @JoinColumn(name = "my_entity_id"))
private List<MonetaryAmount> monetaryAmounts = new ArrayList<>();

When I try to map the same Entity using HBM Files, however the two columns of the composite user type are not recognized.

<list name="monetaryAmounts" table="my_entity_monetary_amounts">
  <key>
    <column name="my_entity_id"/>
  </key>
  <index column="amounts_idx"/>
  <composite-element class="com.example.MonetaryAmountType"/>
</list>

What am I doing wrong? Or is this maybe an Hibernate Bug related to HBM in Hibernate 6.5?

Note that hbm.xml is deprecated for removal, so I would suggest you to not use this anymore and switch to Hibernate orm.xml extensions with this xsd schema: https://hibernate.org/xsd/orm/mapping/mapping-7.0.xsd

AFAIK, in hbm.xml, the composite-element node needs to specify all of the properties that it has as child elements. It’s not enough to list just the class.

Hallo Christian,
Thank you for your answer. I am using Hibernate 6.5, if I understand everything correctly the file you mentioned is not available until hibernate 7.0.

I tried using the xml “ORM” (as in mapping-3.1.xml) without luck, is there an example that shows how this can be achieved?

Thank you in advance

Hi,

there are no examples unfortunately, but since this is an extension of the JPA orm.xml schema, you can mostly rely on tutorials for that e.g. eclipselink-orm.xml Schema Reference | EclipseLink 2.6.x Java Persistence API (JPA) Extensions Reference
The extensions are usually modeled just like the annotations, so if you know what annotations you want to use, just search the XSD to see if you can find the matching XML element that you need to use. In this particular case, you might be able to use the composite-user-type element to register the user type for the java class MonetaryAmount globally.

Hi Christian,
I have already tried with the global composite-user-type tag, either it does not work or I do something wrong.

Is it expected (i.e. by design) that the xml mapping (either hbm or orm) supports only a subset of the features available with annotations? When not I should problably open a bug for this particular issue. What do you think?

Thanks again

The annotation model is the preferred choice, because that is what the majority of users use. hbm.xml is deprecated and didn’t receive support for any of the new features.
On the other hand, the orm.xml extensions are somewhat new i.e. we only added support for that in Hibernate ORM 6, so we might not have full feature parity yet with all the new things that we’re adding.
If you’re missing something, please create a ticket in our issue tracker. Ideally, you would also try to work on the implementation :slight_smile:

Note that so far, we tried to implement orm.xml extensions to have feature parity with hbm.xml, which is limited though and certainly there might be bugs hiding since we’re not testing this very well.

Hi Ciaccia & Christian,

We are facing a similar issue while using CompositeUserType in an hbm.xml file. However, unlike your case, we are not using it in a collection but as a regular variable. Below is how we have defined it in hbm.xml:

 <property generated='never' lazy='false' name='optionexpression' optimistic-lock='false' type='chs.pof.util.TwoStringIndefiniteLenghType' unique='false'>
      <column name='COLUMNNAME_1'/>
      <column name='COLUMNNAME_2'/>
    </property>

While building the SessionFactory, we are encountering the following exception:

Caused by: org.hibernate.MappingException: Property 'chs.pof.Functiondesign.optionexpression' maps to 2 columns but 1 columns are required (type 'chs.pof.util.TwoStringIndefiniteLenghType' spans 1 columns)
	at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:690)
	at org.hibernate.mapping.RootClass.validate(RootClass.java:286)
	at org.hibernate.boot.internal.MetadataImpl.validate(MetadataImpl.java:505)
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:282)
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:463)
	at org.hibernate.boot.internal.MetadataImpl.buildSessionFactory(MetadataImpl.java:210)
	at chs.capitalmanager.pof.HibernateUtil.<init>(HibernateUtil.java:811)
	... 5 more

Additionally, we use Hibernate Tools to generate Java classes from hbm.xml, but that too is failing with a MappingException, stating that it is unable to resolve the class chs.pof.util.TwoStringIndefiniteLenghType. We initially suspected that the custom type might not be compiled before the hbm2java task, so we compiled it beforehand, but the issue still persists.

Ciaccia, could you please share the exception you are encountering?

Thanks in advance for any help!

What you are encountering is documented in the migration guide: hibernate-orm/migration-guide.adoc at 6.0 · hibernate/hibernate-orm · GitHub

You simply can’t have basic types anymore that span multiple columns. You have to use a CompositeUserType instead, so in case of hbm.xml, use

<component class="chs.pof.util.TwoStringIndefiniteLenghType">
      <property name='prop1' column='COLUMNNAME_1'/>
      <property name='prop2' column='COLUMNNAME_2'/>
</component>

Hi Christian

Thanks for your suggestion. I have implemented the change in hbm.xml, but I am now encountering the following exception:

Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    chs/pof/util/TwoStringIndefiniteLenghType$TwoStringMapper$HibernateAccessOptimizer$ptBR3C8s.setPropertyValues(Ljava/lang/Object;[Ljava/lang/Object;)V @11: invokevirtual
  Reason:
    Type 'chs/pof/util/TwoStringIndefiniteLenghType$TwoStringMapper' (current frame, stack[0]) is not assignable to 'chs/pof/util/TwoStringValue'
  Current Frame:
    bci: @11
    flags: { }
    locals: { 'chs/pof/util/TwoStringIndefiniteLenghType$TwoStringMapper$HibernateAccessOptimizer$ptBR3C8s', 'java/lang/Object', '[Ljava/lang/Object;' }
    stack: { 'chs/pof/util/TwoStringIndefiniteLenghType$TwoStringMapper', 'java/lang/String' }
  Bytecode:
    0000000: 2bc0 000c 2c12 0d32 c000 0fb6 0015 2bc0
    0000010: 000c 2c12 1632 c000 0fb6 0019 b1       

	at java.base/java.lang.ClassLoader.defineClass0(Native Method)
	at java.base/java.lang.System$2.defineClass(System.java:2394)
	at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2505)
	at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2480)
	at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1865)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at net.bytebuddy.utility.Invoker$Dispatcher.invoke(Unknown Source)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1033)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1163)
	at jdk.proxy2/jdk.proxy2.$Proxy99.defineClass(Unknown Source)
	at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1685)
	at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.injectRaw(ClassInjector.java:166)
	at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:154)
	at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$UsingLookup.load(ClassLoadingStrategy.java:519)
	at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:101)
	at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6424)
	at org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState.load(ByteBuddyState.java:149)
	at org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl.getReflectionOptimizer(BytecodeProviderImpl.java:244)
	at org.hibernate.metamodel.internal.EmbeddableRepresentationStrategyPojo.buildReflectionOptimizer(EmbeddableRepresentationStrategyPojo.java:249)
	at org.hibernate.metamodel.internal.EmbeddableRepresentationStrategyPojo.<init>(EmbeddableRepresentationStrategyPojo.java:94)
	at org.hibernate.metamodel.internal.ManagedTypeRepresentationResolverStandard.resolveStrategy(ManagedTypeRepresentationResolverStandard.java:156)
	at org.hibernate.metamodel.mapping.internal.EmbeddableMappingTypeImpl.<init>(EmbeddableMappingTypeImpl.java:197)
	at org.hibernate.metamodel.mapping.internal.EmbeddableMappingTypeImpl.from(EmbeddableMappingTypeImpl.java:144)
	at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildEmbeddedAttributeMapping(MappingModelCreationHelper.java:339)
	at org.hibernate.persister.entity.AbstractEntityPersister.buildEmbeddedAttributeMapping(AbstractEntityPersister.java:6021)
	at org.hibernate.persister.entity.AbstractEntityPersister.generateNonIdAttributeMapping(AbstractEntityPersister.java:5954)
	at org.hibernate.persister.entity.AbstractEntityPersister.prepareMappingModel(AbstractEntityPersister.java:5217)
	at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess.execute(MappingModelCreationProcess.java:84)
	at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess.process(MappingModelCreationProcess.java:42)
	at org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl.finishInitialization(MappingMetamodelImpl.java:200)
	at org.hibernate.internal.SessionFactoryImpl.initializeMappingModel(SessionFactoryImpl.java:373)
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:302)
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:463)
	at org.hibernate.boot.internal.MetadataImpl.buildSessionFactory(MetadataImpl.java:210)
	at chs.capitalmanager.pof.HibernateUtil.<init>(HibernateUtil.java:811)
	at chs.capitalmanager.pof.HibernateUtil.init(HibernateUtil.java:744)
	at chs.capitalmanager.appserver.AppServer.<init>(AppServer.java:249)
	at chs.capitalmanager.appserver.AppServer.lambda$main$0(AppServer.java:148)
	at chs.capitalmanager.appserver.AppServer.startEnsuringSingleInstance(AppServer.java:684)
	at chs.capitalmanager.appserver.AppServer.main(AppServer.java:120)

Below is my CompositeUserType class implementation:

public class TwoStringValue{
	private String optionexpression;

	private String optionexpressionClob;

	public TwoStringValue(String optionExpression, String optionExpressionClob) {
		this.optionexpression = optionExpression;
		this.optionexpressionClob = optionExpressionClob;
	}

	public String getOptionexpression() {
		return optionexpression;
	}

	public void setOptionexpression(String optionexpression) {
		this.optionexpression = optionexpression;
	}

	public String getOptionexpressionClob() {
		return optionexpressionClob;
	}

	public void setOptionexpressionClob(String optionexpressionClob) {
		this.optionexpressionClob = optionexpressionClob;
	}

	@Override
	public String value() {
		if (optionexpression.length() <= 1024) {
			return optionexpression;
		}
		else {
			return optionexpressionClob;
		}
	}
}

public class TwoStringIndefiniteLenghType implements CompositeUserType<TwoStringValue>, Serializable
{

	@SuppressWarnings("ALL")
	public static class TwoStringMapper {

		@Column (name = "OPTION_EXPRESSION")
		String optionexpression;
		@Column (name = "OPTION_EXPRESSION_CLOB")
		String optionexpressionClob;

		public String getOptionexpression() {
			return optionexpression;
		}

		public String getOptionexpressionClob() {
			return optionexpressionClob;
		}
	}

	public TwoStringIndefiniteLenghType()
	{
	}

	@Override
	public Object getPropertyValue(TwoStringValue twoStringValue, int i) throws HibernateException {
		if (i == 0) {
			return twoStringValue.getOptionexpression();
		} else if (i == 1) {
			return twoStringValue.getOptionexpressionClob();
		}
		throw new HibernateException("Invalid property index: " + i);
	}

	@NotNull
	@Override
	public TwoStringValue instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactoryImplementor) {
		String firstColumnValue = valueAccess.getValue(0, String.class);
		String secondColumnValue = valueAccess.getValue(1, String.class);
        return new TwoStringValue(firstColumnValue, secondColumnValue);
    }

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

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

	@Override
	public boolean equals(TwoStringValue twoStringValue, TwoStringValue j1) {
		return Objects.equals(twoStringValue, j1);
	}

	@Override
	public int hashCode(TwoStringValue twoStringValue) {
		return twoStringValue.hashCode();
	}

	@Override
	public TwoStringValue deepCopy(TwoStringValue twoStringValue) {
		return twoStringValue;
	}

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

	@Override
	public Serializable disassemble(TwoStringValue twoStringValue) {
		return new String[]{twoStringValue.getOptionexpression(), twoStringValue.getOptionexpressionClob()};
	}

	@Override
	public TwoStringValue assemble(Serializable serializable, Object o) {
		String[] serializable1 = (String[]) serializable;
		return new TwoStringValue(serializable1[0], serializable1[1]);
	}

	@Override
	public TwoStringValue replace(TwoStringValue twoStringValue, TwoStringValue j1, Object o) {
		return twoStringValue;
	}
}

I don’t understand what I’m doing wrong. I found some help online, and if I return TwoStringValue instead of TwoStringMapper in the embeddable() method, it works fine. However, I followed the Using CompositeUserType documentation, so I’m not sure why this is happening.

Any insights?

Hi Yashwanth,
We didn’t experience any “Hibernate” exception. In our case, we just had an IndexOutOfBounds exception in our custom type class, since hibernate initialized our custom type without any columns.

Have you tried to add an empty constructor to your TwoStringValue class?

Cheers

This looks good to me, but note that Hibernate ORM 6.5 is not supported anymore, so please update to the latest version first before discussing further.