18

I use Spring Data and decided that I want to create new custom data type that can be used in Hibernate entities. I checked the documentation and choose BasicType and implemented it according to this official user guide.

I wanted to be able to register the type under its class name and be able to use the new type in entities without need for @Type annotation. Unfortunately, I’m unable to get reference to the MetadataBuilder or Hibernate configuration to register the new type. Is there a way how to get it in Spring Data? It seems that initialization of the Hibernate is hidden from the user and cannot be easily accessed. We use following class to initialize the JPA:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        basePackages = {
                "..." // omitted
        }
)
public class JpaConfiguration implements TransactionManagementConfigurer {

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean configureEntityManagerFactory(
        DataSource dataSource,
        SchemaPerTenantConnectionProviderImpl provider) {

        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setPersistenceUnitName("defaultPersistenceUnit");
        entityManagerFactoryBean.setDataSource(dataSource);
        entityManagerFactoryBean.setPackagesToScan(
                "..." // omitted
        );
        entityManagerFactoryBean.setJpaProperties(properties(provider));
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return entityManagerFactoryBean;
    }

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new JpaTransactionManager();
    }

    private Properties properties(SchemaPerTenantConnectionProviderImpl provider) {
        Properties properties = new Properties();
        // omitted
        return properties;
    }
}

I found lots of articles about way how to do it with Hibernate’s Configuration object but this one refers to Hibernate 3 and 4. I also found way how to do it via Hibernate org.hibernate.integrator.spi.Integrator but when I use it according to the articles I found I will get exception with the message “org.hibernate.HibernateException: Can not alter TypeRegistry at this time” What is the correct way to register custom types in Spring Data?

xMort
  • 1,545
  • 3
  • 11
  • 20

7 Answers7

15

I finally figured it out. I will post it here for others:

I created a new class that implements org.hibernate.boot.spi.SessionFactoryBuilderFactory interface. In this class I can get reference to the TypeResolver from metadata and register my custom type.

package com.example.configuration;

import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.boot.spi.SessionFactoryBuilderFactory;
import org.hibernate.boot.spi.SessionFactoryBuilderImplementor;
import org.slf4j.LoggerFactory;

import com.example.CustomType;

public class CustomDataTypesRegistration implements SessionFactoryBuilderFactory {

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CustomDataTypesRegistration.class);

    @Override
    public SessionFactoryBuilder getSessionFactoryBuilder(final MetadataImplementor metadata, final SessionFactoryBuilderImplementor defaultBuilder) {
        logger.info("Registering custom Hibernate data types");
        metadata.getTypeResolver().registerTypeOverride(CustomType.INSTANCE);
        return defaultBuilder;
    }
}

The class must be then registered via Java ServiceLoader mechanism by adding full name of the class with its packages into the file with name org.hibernate.boot.spi.SessionFactoryBuilderFactory into the java module’s META-INF/services directory:

src/main/resources/META-INF/services/org.hibernate.boot.spi.SessionFactoryBuilderFactory

The file can contain multiple lines, each referencing different class. In this case it is:

com.example.configuration.CustomDataTypesRegistration 

This way the Spring Data starts and custom type is successfully registered during Hibernate initialization.

What helped my quite a lot was this SO answer that deals with schema export in Hibernate 5 under Spring Data.

Community
  • 1
  • 1
xMort
  • 1,545
  • 3
  • 11
  • 20
  • 6
    It took you around 20 secs between asking and answering, I guess "*I finally figured it out*" is a joke, right? ;P – Mistalis Mar 09 '17 at 12:38
  • 10
    I wrote the question yesterday, but did not post it. Today I figured it out. I decide to post it with answer for others that will encounter the same issue. For me there was delay before the post ;) – xMort Mar 09 '17 at 16:42
13

There's a much easier solution to this -- in fact, it's just 1 line of code. You can just use the @TypeDef annotation and thus avoid having to register the custom type:

@Entity(name = "Product")
@TypeDef(
    name = "bitset",
    defaultForType = BitSet.class,
    typeClass = BitSetType.class
)
public static class Product {

    @Id
    private Integer id;

    private BitSet bitSet;

For an example, see "Example 11. Using @TypeDef to register a custom Type" in http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html

user64141
  • 5,141
  • 4
  • 37
  • 34
9

I use JPA with Spring 4.3.9 and Hibernate 5.0.5 and I use custom property EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS with Spring LocalContainerEntityManagerFactoryBean to override Hibernate BasicTypes.

final Properties jpaProperties = new Properties();
jpaProperties.put(EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS, new TypeContributorList() {
    @Override
    public List<TypeContributor> getTypeContributors() {
         return Lists.newArrayList(new CustomDateTimeTypeContributor());
    }
});
final LocalContainerEntityManagerFactoryBean factoryBean = new 
LocalContainerEntityManagerFactoryBean();
factoryBean.setJpaProperties(jpaProperties);
factoryBean.setJpaVendorAdapter(jpaVendorAdapter);
return factoryBean;
alex.tran
  • 111
  • 1
  • 3
8

An alternative to what xMort did, can be registering a org.hibernate.boot.model.TypeContributor via ServiceLoader mechanism.

  1. Implement TypeContributor
package com.example.configuration;

import org.hibernate.boot.model.TypeContributions;
import org.hibernate.boot.model.TypeContributor;
import org.hibernate.service.ServiceRegistry;

public class CustomTypeContributor implements TypeContributor {
    @Override
    public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
        typeContributions.contributeType(CustomType.INSTANCE);
    }
}

  1. Create a file org.hibernate.boot.model.TypeContributor into the java module’s META-INF/services
src/main/resources/META-INF/services/org.hibernate.boot.model.TypeContributor
  1. Reference the TypeContributor, file content:
ru.eastbanctech.scs.air.transaction.repositories.StringArrayTypeContributor

As of Hibernate 5.2.17, the code that picks up the TypeContributor service can be found at org.hibernate.boot.model.process.spi.MetadataBuildingProcess#handleTypes.

cinereo
  • 107
  • 2
  • 3
  • Feels odd, but that is the best solution I found where I don't have to annotate the domain values to use the UserType. Does anyone know a more Spring-like solution? – Valerij Dobler May 25 '23 at 18:47
2

Inspired by @alex.tran's answer:

@Bean
public HibernatePropertiesCustomizer customHibernateTypeRegistrar() {
    return (Map<String, Object> props) -> {
        props.put(
                EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS,
                (TypeContributorList) () -> Arrays.asList((TypeContributor) (typeContributions, serviceRegistry) -> {
                    // Deregister built-in org.hibernate.type.OffsetDateTimeSingleColumnType as it hides our mapping.
                    final BasicTypeRegistry basicTypeRegistry = typeContributions.getTypeConfiguration().getBasicTypeRegistry();
                    Class<OffsetDateTime> clazz = OffsetDateTime.class;
                    basicTypeRegistry.unregister(clazz.getName());
                    basicTypeRegistry.unregister(clazz.getSimpleName());

                    typeContributions.contributeSqlTypeDescriptor(OffsetDateTimeMsSqlTypeDescriptor.INSTANCE);
                    typeContributions.contributeJavaTypeDescriptor(OffsetDateTimeJavaTypeDescriptor.INSTANCE);
                    typeContributions.contributeType(OffsetDateTimeSingleColumnType.INSTANCE);
                }));
    };
}

Magical constant EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS & bean of type HibernatePropertiesCustomizer do the magic in EntityManagerFactoryBuilderImpl:

HibernateJpaAutoConfiguration -> HibernateJpaConfiguration -> JpaBaseConfiguration -> LocalContainerEntityManagerFactoryBean -> HibernatePersistenceProvider -> Bootstrap.getEntityManagerFactoryBuilder() -> EntityManagerFactoryBuilderImpl.

See details: https://discourse.hibernate.org/t/map-ms-sql-server-datetimeoffset-to-java-8-offsetdatetime/5937/7

Solution keeps Boot magic intact as it plugs into Spring Boot autoconfiguration, also there is no need to redefine lots of beans (like transaction or entity manager).

gavenkoa
  • 45,285
  • 19
  • 251
  • 303
2

You can use the HibernatePropertiesCustomizer class spring will load all beans of this type (for more details see HibernateJpaConfiguration)

Also see PG Json config Test

this is an example (I'm adding Json type only in case postgress dialect is configured)

@Bean
@ConditionalOnProperty(value = "spring.jpa.properties.hibernate.dialect",
    havingValue = "org.hibernate.dialect.PostgreSQL10Dialect")
public HibernatePropertiesCustomizer hibernatePropertiesCustomizerPG(ObjectMapper objectMapper) {
    return hibernateProperties -> {
        hibernateProperties.put(EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS,
            (TypeContributorList) () -> List.of(
                (TypeContributor) (TypeContributions typeContributions, ServiceRegistry serviceRegistry) ->
                    typeContributions.contributeType(new JsonType(objectMapper))));
    };
}

This is the same than have

@TypeDefs({
    @TypeDef(name = "json", typeClass = JsonType.class)
})
@MappedSuperclass
public class BaseEntity {
    //Code omitted for brevity
}

I prefer to do in this way to not override any bean handled by spring such as SessionFactory

Miguel Galindo
  • 111
  • 1
  • 3
0

There is another solution.

  1. We extend Hibernate MetadaSources overriding methods getMetadataBuilder by creating MetadataBuilder:
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.internal.MetadataBuilderImpl;
import org.hibernate.boot.registry.StandardServiceRegistry;

public class CustomTypeIncludedMetadataSources extends MetadataSources {

    @Override
    public MetadataBuilder getMetadataBuilder() {
        MetadataBuilder b = new MetadataBuilderImpl(this);
        applyCustomTypes(b);
        return b;
    }

    @Override
    @Deprecated
    public MetadataBuilder getMetadataBuilder(StandardServiceRegistry serviceRegistry) {
        MetadataBuilder b = new MetadataBuilderImpl(this, serviceRegistry);
        applyCustomTypes(b);
        return b;
    }

    private void applyCustomTypes(MetadataBuilder b) {
        b.applyBasicType(new CustomType());
        ...
    }
}

  1. Next, in Spring @Configuration class we set an instance of our CustomTypeIncludedMetadataSources into Spring LocalSessionFactoryBean:
@Bean
public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
    LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
    sessionFactory.setMetadataSources(new CustomTypeIncludedMetadataSources());
    sessionFactory.setDataSource(dataSource);
    ...
    return sessionFactory;
}
pto3
  • 513
  • 1
  • 5
  • 13