grails / grails-boot

Grails integrations with Spring Boot
Apache License 2.0
32 stars 13 forks source link

really hard to initialize second datasource with HibernateGormAutoConfiguration #5

Open benheilers opened 9 years ago

benheilers commented 9 years ago

Hi, I couldn't find the issue tracker for gorm-hibernate4-spring-boot, so please help me find it if this one is GSP-specific.

I wanted to set up a second data source (primary data source against postgres and second datasource against Vertica). We have the same code in a Grails project that works fine.

Here are the issues I have found so far:

  1. HibernateGormAutoConfiguration is hardcoded as:
        initializer = new HibernateDatastoreSpringInitializer(classLoader, packages as String[])
        initializer.resourceLoader = resourceLoader
        initializer.setConfiguration(getDatastoreConfiguration())
        initializer.configureForBeanDefinitionRegistry(registry)

But HibernateDatastoreSpringInitializer is hardcoded as:

    String defaultDataSourceBeanName = "dataSource"
    Set<String> dataSources = [defaultDataSourceBeanName]

So adding a second datasource actually required: a) subclassing HibernateGormAutoConfiguration with my own auto configuration class that copies/pastes the code except to insert the line:

        //NOTE: change from parent class, add vertica data source
        initializer.dataSources = [ 'dataSource', 'dataSource_vertica' ]

b) add HibernateGormAutoConfiguration to the excludes list of auto configuration, or else both run

  1. HibernateGormAutoConfiguration.EagerInitProcessor has the code:

But the PostInitializingHandling bean is registered once per datasource, we can see this in a for loop over datasources:

                "org.grails.gorm.hibernate.internal.POST_INIT_BEAN-${dataSourceName}$suffix"(PostInitializationHandling) { bean ->
                    grailsApplication = ref(GrailsApplication.APPLICATION_ID)
                    bean.lazyInit = false
                }

So we end up with an exception:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [grails.orm.bootstrap.HibernateDatastoreSpringInitializer$PostInitializationHandling] is defined: expected single matching bean but found 2: org.grails.gorm.hibernate.internal.POST_INIT_BEAN-dataSource_vertica_dataSource_vertica,org.grails.gorm.hibernate.internal.POST_INIT_BEAN-dataSource
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:365)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:331)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:968)
    at org.grails.datastore.gorm.boot.autoconfigure.HibernateGormAutoConfiguration$EagerInitProcessor.postProcessBeforeInitialization(HibernateGormAutoConfiguration.groovy:115)
  1. The datasource names are a little wonky. In HibernateDatastoreSpringInitializer there is code like this:
            for(dataSourceName in dataSources) {

                boolean isDefault = dataSourceName == defaultDataSourceBeanName
                String suffix = isDefault ? '' : '_' + dataSourceName
                String prefix = isDefault ? '' : dataSourceName + '_'
                def sessionFactoryName = isDefault ? defaultSessionFactoryBeanName : "sessionFactory$suffix"
                def hibConfig = configurationObject["hibernate$suffix"] ?: configurationObject["hibernate"]

...
                        dataSource = ref(dataSourceName)

For my second data source I desire the ultimate data source's name to be "dataSource_vertica". So that's the name I've put into the HibernateDatastoreSpringInitializer.dataSources field.

But you can see above the suffix has been determined as _dataSource_vertica, so now the session factory has been named "sessionFactory_dataSource_vertica" instead of just "sessionFactory_vertica". And it's looking for hibernate properties "hibernate_dataSource_vertica".

So I think the "suffix" and "prefix" variables should not include dataSourceName.

  1. In my application.yml I have:
  hibernate:
    hbm2ddl.auto: ''
    dialect: mycompany.hibernate.dialect.PostgresSequencePerTableDialect
  hibernate_dataSource_vertica:
    hbm2ddl.auto: ''
    dialect: mycompany.hibernate.dialect.VerticaDialect

But as I showed in #1, HibernateGormAutoConfiguration has:

        initializer.setConfiguration(getDatastoreConfiguration())

where getDatastoreConfiguration() is defined as:

    protected Properties getDatastoreConfiguration() {
        if(environment != null) {
            def config = environment.getSubProperties("hibernate.")
            def properties = new Properties()
            for(entry in config.entrySet()) {
                properties.put("hibernate.${entry.key}".toString(), entry.value)
            }
            return properties
        }
    }

You see the same data store configuration is used for both datasources, whereas I wanted to use a different one for each.

benheilers commented 9 years ago

I also thought HibernateDatastoreSpringInitializer#configureForDataSource() was a really cool method, until I saw that is is hard-coded to register the "dataSource" bean:

        applicationContext.beanFactory.registerSingleton(defaultDataSourceBeanName, dataSource)
benheilers commented 9 years ago

What appears to be another bug in HibernateDatastoreSpringInitializer:

            if(!beanDefinitionRegistry.containsBeanDefinition("entityInterceptor")) {
                entityInterceptor(EmptyInterceptor)
            }
...
                        entityInterceptor = ref("entityInterceptor$suffix")

So I get an exception: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityInterceptor_dataSource_vertica' is defined

If it's intended that we define the beans ourselves, then please document this somewhere.

Thanks!

benheilers commented 9 years ago

And there's also this code:

                if (!beanDefinitionRegistry.containsBeanDefinition("transactionManager")) {
                    "transactionManager$suffix"(GrailsHibernateTransactionManager) { bean ->
                        bean.autowire = "byName"
                        sessionFactory = ref(sessionFactoryName)
                        dataSource = ref(dataSourceName)
                    }
                }

I had to initialize the HibernateDatastoreSpringInitializer with the datasources in the opposite order to make sure that "transactionManager_dataSource_vertica" is registered before "transactionManager":

        //NOTE: change from parent class, add vertica data source, force ordering of vertica datasource first
        initializer.dataSources = [ 'dataSource_vertica', 'dataSource'  ] as LinkedHashSet<String>
benheilers commented 9 years ago

HibernatePluginSupport made sure to add these properties to the session factory:

                dataSourceName = datasourceName
                sessionFactoryBeanName = "sessionFactory$suffix"

Without them I end up with two ConfigurableLocalSessionFactoryBeans which both have field "dataSourceName" set to "DEFAULT" even though the second bean has field "dataSource" set to the vertica data source. This of course causes problems in building the session factory proxy.

Am I doing something wrong?