Custom types not working since HHH-14335 (introduced in 5.4.25)

Hello

We have been testing the newest Spring Boot version (2.4.1) that bumps Hibernate to version 5.4.25.
We also use Jadira User Types to register custom types for Joda Time classes.
Since 5.4.25 it seems that the custom types are no longer working as espected.
We tried debugging this issue, and it seems to be tied to the fix for HHH-14335.

Our findings are as follows:

  1. During initialization, the SPI classes are found:
	  at org.jadira.usertype.dateandtime.joda.integrator.UserTypeJodaTimeHibernateIntegrator.<clinit>(UserTypeJodaTimeHibernateIntegrator.java:44)
      [...]
	  at org.hibernate.boot.registry.classloading.internal.AggregatedServiceLoader$ClassPathAndModulePathAggregatedServiceLoader.collectServiceIfNotDuplicate(AggregatedServiceLoader.java:276)
	  at org.hibernate.boot.registry.classloading.internal.AggregatedServiceLoader$ClassPathAndModulePathAggregatedServiceLoader.loadAll(AggregatedServiceLoader.java:201)
	  at org.hibernate.boot.registry.classloading.internal.AggregatedServiceLoader$ClassPathAndModulePathAggregatedServiceLoader.getAll(AggregatedServiceLoader.java:187)
	  at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.loadJavaServices(ClassLoaderServiceImpl.java:251)
	  at org.hibernate.integrator.internal.IntegratorServiceImpl.<init>(IntegratorServiceImpl.java:40)
	  at org.hibernate.boot.registry.BootstrapServiceRegistryBuilder.build(BootstrapServiceRegistryBuilder.java:224)
	  at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.buildBootstrapServiceRegistry(EntityManagerFactoryBuilderImpl.java:452)
	  at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.<init>(EntityManagerFactoryBuilderImpl.java:208)
	  at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.<init>(EntityManagerFactoryBuilderImpl.java:168)
  1. There appears to be a metadata validation step that will eagerly initialize all types:
	  at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:484)
	  at org.hibernate.mapping.SimpleValue.isValid(SimpleValue.java:466)
	  at org.hibernate.mapping.Property.isValid(Property.java:227)
	  at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:624)
	  at org.hibernate.mapping.RootClass.validate(RootClass.java:267)
	  at org.hibernate.boot.internal.MetadataImpl.validate(MetadataImpl.java:354)
	  at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465)
	  at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1259)

The heuristicType call on org.hibernate.mapping.SimpleValue.getType() returns org.hibernate.type.SerializableType, which is incorrect.
However, critically, in 5.4.24 and earlier versions, this result was not cached.

  1. User types are then loaded from the SPI classes:
	  at org.jadira.usertype.dateandtime.joda.integrator.UserTypeJodaTimeHibernateIntegrator.getUserTypes(UserTypeJodaTimeHibernateIntegrator.java:97)
	  at org.jadira.usertype.spi.shared.AbstractUserTypeHibernateIntegrator.autoRegisterUsertypes(AbstractUserTypeHibernateIntegrator.java:135)
	  at org.jadira.usertype.spi.shared.AbstractUserTypeHibernateIntegrator.integrate(AbstractUserTypeHibernateIntegrator.java:203)
	  at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:282)
	  at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:469)

Before the fix for HHH-14335 the types were not cached, so this was not problematic, since later the type would be recreated as a org.hibernate.type.CustomType on SimpleValue.

  1. Later org.hibernate.mapping.SimpleValue.getType() gets called again:
	  at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:484)
	  at org.hibernate.tuple.PropertyFactory.buildEntityBasedAttribute(PropertyFactory.java:159)
	  at org.hibernate.tuple.entity.EntityMetamodel.<init>(EntityMetamodel.java:231)
	  at org.hibernate.persister.entity.AbstractEntityPersister.<init>(AbstractEntityPersister.java:609)
	  at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:128)
	  [...]
	  at org.hibernate.persister.internal.PersisterFactoryImpl.createEntityPersister(PersisterFactoryImpl.java:96)
	  at org.hibernate.persister.internal.PersisterFactoryImpl.createEntityPersister(PersisterFactoryImpl.java:77)
	  at org.hibernate.metamodel.internal.MetamodelImpl.initialize(MetamodelImpl.java:181)
	  at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:301)
	  at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:469)
	  at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1259)

On version 5.4.24, the heuristicType call returns the correct type: org.hibernate.type.CustomType
Whereas on version 5.4.25 and later the call does not happen at all, since the earlier incorrect type is cached.

I’m unsure about the protocol here. Should I try to open a ticket directly on hibernate.atlassian.net?

(Edited to change stacktraces to be more readable)

Thanks for the report. I created [HHH-14408] SPI provided user types are not applied - Hibernate JIRA for this.

Hi

I see that 5.4.28 was released, seemingly fixing the issue. Thank you for that!

I tested 5.4.28, but unfortunately the issue persists.
While the original conditions for triggering the issue seem resolved, we also use Hibernate Envers, and that appears to still trigger the issue.

It appears that Hibernate Envers also triggers types to be initialized too early (before Jadira types are loaded):

"main@1" prio=5 tid=0x1 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
	  at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:474)
	  at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.addValueInFirstPass(AuditMetadataGenerator.java:189)
	  at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.addValue(AuditMetadataGenerator.java:316)
	  at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.addProperties(AuditMetadataGenerator.java:350)
	  at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.generateFirstPass(AuditMetadataGenerator.java:657)
	  at org.hibernate.envers.configuration.internal.EntitiesConfigurator.configure(EntitiesConfigurator.java:95)
	  at org.hibernate.envers.boot.internal.EnversServiceImpl.doInitialize(EnversServiceImpl.java:158)
	  at org.hibernate.envers.boot.internal.EnversServiceImpl.initialize(EnversServiceImpl.java:122)
	  at org.hibernate.envers.boot.internal.AdditionalJaxbMappingProducerImpl.produceAdditionalMappings(AdditionalJaxbMappingProducerImpl.java:101)
	  at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:306)

In summary, because of Envers, custom types such as the Jadira provided types are still being cached as org.hibernate.type.SerializableType, and not org.hibernate.type.CustomType in 5.4.28.

Well, the main problem here is that Jadira is just registering types at a point where it is “too late”. Note that we actually plan to remove support for registering types in an Integrator in Hibernate 6.0. I created and issue for this in the Jadira repository: Switch to using MetadataContributor or TypeContributor for contributing types · Issue #100 · JadiraOrg/jadira · GitHub

Please note that Envers could behave in unexpected ways due to this. The only real solution is to register these types in a MetadataContributor or TypeContributor. If the maintainers of Jadira don’t adapt to that, you will have to write this yourself. I don’t see how we can avoid creating the types in the Envers integration.

Hi

Thanks for your prompt reply.
Unfortunately the Jadira project appears dormant, so I cannot place much hope in it being updated to use the new Hibernate APIs.

I have familiarized myself a bit more with TypeContributors, and we can get around the issue by:

  1. Disabling Jadira’s own automatic registration process by setting the JPA property jadira.usertype.autoRegisterUserTypes: false
  2. Using a custom TypeContributor that registers the Jadira custom types. As a reference to anyone that might bump into this report, I added an example of how to do it for JodaTime here.
  3. Register the new custom TypeContributor by creating the file META-INF/services/org.hibernate.boot.model.TypeContributor and add the fully qualified class name of the created type contributor.

I should say that for completeness sake, it appears that such a solution might not be supported much longer, as the API call org.hibernate.boot.model.TypeContributions#contributeType(org.hibernate.usertype.UserType, java.lang.String...) is clearly marked deprecated.

As such, we won’t be used this particular solution, and we opted instead to create our own org.hibernate.type.BasicType implementations for the few JodaTime types we use.

We are in the process of testing this solution, but so far all appears to work well.

Again, thanks for you time with this!