In distributed enterprise architectures, applications rarely interact with just a single database. Systems often require multiple physical datasources: separating high-throughput transactional OLTP databases from read-heavy analytical reporting replicas, integrating legacy databases during multi-year migrations, or isolating customer data in multi-tenant environments. In the Groovy on Grails framework, configuring secondary datasources—whether via the classic DataSources plugin or native Grails multi-datasource support—frequently produces a notorious runtime exception that leaves engineers bewildered: nested exception is org.hibernate.cache.NoCachingEnabledException: Second-level cache is not enabled for usage [hibernate.cache.use_second_level_cache | hibernate.cache.use_query_cache]. Below is an exhaustive root cause analysis of this Hibernate configuration conflict, complete with a step-by-step resolution runbook, cache region factory configurations, domain mapping patterns, and transaction management best practices.
1. Root Cause Analysis of NoCachingEnabledException
To understand why this error occurs, one must understand how Hibernate initializes its caching infrastructure across multiple session factories. In Grails, each configured datasource creates an independent SessionFactory bean inside the Spring application context:
- The primary
dataSourcecreates the defaultsessionFactory. - Secondary datasources (e.g.
dataSource_reporting,dataSource_lookup) create dedicated session factories (e.g.sessionFactory_reporting).
The NoCachingEnabledException occurs when a domain entity mapped to a secondary datasource requests Hibernate second-level caching (via cache: true or usage: 'read-only' in its mapping block), but the secondary SessionFactory configuration either:
- Lacks an explicitly configured second-level cache provider (such as Ehcache or Hazelcast).
- Inherits global caching directives while missing the underlying cache region factory binding in its Spring datasource configuration.
- Attempts to utilize the query cache (
use_query_cache = true) without having the base second-level cache enabled (use_second_level_cache = true) for that specific session factory instance.
Under the hood, Hibernate's internal SettingsFactory builds independent Settings objects for each session factory bean. When a secondary datasource is registered without its own explicit cache region factory binding, Hibernate defaults to disabling second-level caching for that isolated session context. When a domain entity configured with cache: true is subsequently loaded through that secondary session factory, the runtime detects the missing cache implementation and throws NoCachingEnabledException to prevent partial, un-cached data inconsistencies.
2. Correct Multiple DataSource Configuration in DataSource.groovy
The resolution requires configuring both the primary and secondary datasources with explicit Hibernate cache settings in grails-app/conf/DataSource.groovy:
// grails-app/conf/DataSource.groovy
hibernate {
cache.use_second_level_cache = true
cache.use_query_cache = true
// Specify the modern EhCache Region Factory:
cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory'
singleSession = true // Shared session per transaction
}
// ── Primary Transactional DataSource ──────────────────────────────────────────
dataSource {
pooled = true
driverClassName = "com.mysql.cj.jdbc.Driver"
dialect = org.hibernate.dialect.MySQL8Dialect
username = "app_user"
password = "app_password"
dbCreate = "update"
url = "jdbc:mysql://localhost:3306/primary_production?useSSL=false&serverTimezone=UTC"
}
// ── Secondary Reporting / Read-Only DataSource ────────────────────────────────
dataSource_reporting {
pooled = true
driverClassName = "com.mysql.cj.jdbc.Driver"
dialect = org.hibernate.dialect.MySQL8Dialect
username = "readonly_user"
password = "readonly_password"
dbCreate = "none" // Never auto-update read-only or legacy schemas!
url = "jdbc:mysql://replica-host:3306/reporting_replica?useSSL=false&serverTimezone=UTC"
}
// ── Environment-Specific Hibernate Overrides ─────────────────────────────────
environments {
production {
dataSource {
properties {
maxActive = 50
maxIdle = 25
minIdle = 5
initialSize = 5
testOnBorrow = true
validationQuery = "SELECT 1"
}
}
dataSource_reporting {
properties {
maxActive = 20
maxIdle = 10
minIdle = 2
testOnBorrow = true
validationQuery = "SELECT 1"
}
}
}
}
3. Domain Class Mapping: Explicit DataSource & Cache Binding
When mapping domain classes in a multi-datasource Grails application, you must explicitly declare which datasource owns the entity, and ensure cache configurations match the capabilities of that datasource:
// grails-app/domain/com/enterprise/analytics/DailyReportSummary.groovy
package com.enterprise.analytics
class DailyReportSummary {
Date reportDate
String businessUnit
Long totalTransactions
BigDecimal totalRevenue
static constraints = {
reportDate nullable: false
businessUnit blank: false
totalTransactions min: 0L
totalRevenue min: 0.00
}
static mapping = {
// Explicitly bind this entity to the reporting secondary datasource:
datasource 'reporting'
table 'rpt_daily_summaries'
version false // Frequently false for legacy or external reporting schemas
// ✅ If second-level cache is enabled, configure read-only caching safely:
cache usage: 'read-only'
}
}
Crucial Rule: If a secondary datasource connects to an external or legacy database where second-level caching is not supported or desired, you must explicitly disable caching for that domain entity by omitting the cache block or setting cache false in its static mapping:
static mapping = {
datasource 'reporting'
table 'rpt_daily_summaries'
cache false // Avoids NoCachingEnabledException when secondary datasource lacks cache factory
}
4. Configuring Ehcache Region XML (ehcache.xml)
When using EhCacheRegionFactory, ensure that grails-app/conf/ehcache.xml is present on the classpath with proper default and named cache definitions:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false"
monitoring="autodestroy">
<diskStore path="java.io.tmpdir/grails-ehcache"/>
<!-- Default cache configuration applied when named cache is omitted -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
<!-- Cache region dedicated to reporting summaries -->
<cache name="com.enterprise.analytics.DailyReportSummary"
maxElementsInMemory="5000"
eternal="false"
timeToLiveSeconds="3600"
overflowToDisk="false"/>
</ehcache>
5. Multi-DataSource Transaction Management Best Practices
When executing transactions across multiple datasources, standard Spring @Transactional annotations bind exclusively to the primary datasource unless configured otherwise:
- Targeted Transactions: To wrap business logic in a transaction targeting a secondary datasource, specify the transaction manager bean name:
import org.springframework.transaction.annotation.Transactional class ReportingService { // Binds explicitly to the secondary transaction manager: @Transactional(transactionManager = "transactionManager_reporting", readOnly = true) List<DailyReportSummary> generateQuarterlyAudit() { return DailyReportSummary.list([sort: 'reportDate', order: 'desc']) } } - Two-Phase Commit (2PC / JTA): If a business process must write atomically to both the primary database and a secondary database in a single transaction, standard JDBC transactions are insufficient. You must configure a Java Transaction API (JTA) distributed transaction manager (such as Atomikos or Bitronix) with XA-compliant JDBC drivers to guarantee atomicity.
