In my Grails 5.2 application I have a "hybrid" multi-tenant configuration, some domain are mapped to the default datasource without a multitenant implementation (these domains are for common tables, eg. configuration parameters, users, roles, etc.), other are built with multitenant implementation and are referred to different phisical databases, each one for a specific customer.
Here my "application.yml" configuration
grails:
profile: web
codegen:
defaultPackage: bilce
# spring:
# transactionManagement:
# proxies: false
gorm:
reactor:
# Whether to translate GORM events into Reactor events
# Disabled by default for performance reasons
events: false
multiTenancy:
mode: DATABASE
tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SessionTenantResolver
---
hibernate:
cache:
queries: false
use_second_level_cache: false
use_query_cache: false
region:
factory_class: 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory'
hbm2ddl:
jdbc_metadata_extraction_strategy: individually
---
dataSource:
pooled: true
jmxExport: true
driverClassName: org.h2.Driver
username: sa
password: ''
---
environments:
development:
dataSource:
dbCreate: create-drop
url: jdbc:h2:mem:defaultDB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
dataSources:
dataSource_customer_01:
dbCreate: create-drop
url: jdbc:h2:mem:customer_01_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
dataSource_customer_02:
dbCreate: create-drop
url: jdbc:h2:mem:customer_02_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
dataSource_customer_03:
dbCreate: create-drop
url: jdbc:h2:mem:customer_03_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
And here my domains: "DefaultDatasourceDomain", mapped to the default datasource, without multitenant implementation
class DefaultDatasourceDomain {
String description
static constraints = {
}
static mapping = {
datasource ConnectionSource.DEFAULT
}
}
And "CustomerDatasourceDomain" which implements Multitenant
class CustomerDatasourceDomain implements MultiTenant<CustomerDatasourceDomain> {
String description
static constraints = {
}
static mapping = {
}
}
Now if I try to query on both domains (the one with Multitenant is pointed to a specific datasource by the right tenant resolver) inside the same action marked with "@Transactional" I get a "no transaction is in progress" error referred to the default datasource, but if I remove the "@Transactional" annotation everything works well (even if the annotation remains present but I do the query on default datasource inside a "withTransaction" block).
This is my controller code:
class TestController {
def index() {
session.setAttribute(SessionTenantResolver.ATTRIBUTE, "dataSource_customer_01")
redirect action: "doWork"
}
// if you mark the action with "@Transactional" annotation you get "no transaction is in progress" at the end
// of the action execution on "DefaultDatasourceDomain", it seems no transaction are opened for default datasource
@Transactional
@CurrentTenant
def doWork() {
println("----------------------------------")
println("DefaultDatasourceDomain contains " + DefaultDatasourceDomain.count() + " records")
println("CustomerDatasourceDomain contains " + CustomerDatasourceDomain.count() + " records")
println("----------------------------------")
redirect uri: "/"
}
}
Now in my "real world" case I have a big application developed with Grails 3.3 with hundreds of these situations working without problems, but now I'm trying to upgrade to Grails 5 and I have these problems I can't solve.
This is my stacktrace:
Caused by: javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:445)
at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3483)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1399)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1394)
at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:113)
at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:97)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:916)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:727)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
at it.testapp.TestController.$mt__doWork(TestController.groovy)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:107)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1268)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1035)
at org.codehaus.groovy.runtime.InvokerHelper.invokePogoMethod(InvokerHelper.java:1029)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:1012)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethodSafe(InvokerHelper.java:101)
at it.testapp.TestController$_doWork_closure1.doCall(TestController.groovy)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:107)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:274)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1035)
at groovy.lang.Closure.call(Closure.java:412)
at groovy.lang.Closure.call(Closure.java:428)
at grails.gorm.multitenancy.Tenants$_withId_closure2$_closure6.doCall(Tenants.groovy:265)
at grails.gorm.multitenancy.Tenants$_withId_closure2$_closure6.call(Tenants.groovy)
at org.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:297)
at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:241)
at org.grails.orm.hibernate.GrailsHibernateTemplate.executeWithNewSession(GrailsHibernateTemplate.java:153)
at org.grails.orm.hibernate.GrailsHibernateTemplate.executeWithExistingOrCreateNewSession(GrailsHibernateTemplate.java:207)
at org.grails.orm.hibernate.AbstractHibernateDatastore.withNewSession(AbstractHibernateDatastore.java:369)
at grails.gorm.multitenancy.Tenants$_withId_closure2.doCall(Tenants.groovy:258)
at grails.gorm.multitenancy.Tenants$_withId_closure2.call(Tenants.groovy)
at grails.gorm.multitenancy.Tenants$CurrentTenant.withTenant(Tenants.groovy:358)
at grails.gorm.multitenancy.Tenants.withId(Tenants.groovy:236)
at org.grails.datastore.gorm.services.DefaultTenantService.withCurrent(DefaultTenantService.groovy:71)
at it.testapp.TestController.doWork(TestController.groovy)
... 55 common frames omitted