-
-
Notifications
You must be signed in to change notification settings - Fork 971
Description
Summary
Data Service query methods that use DetachedCriteria internally ignore the class-level @Transactional(connection = '...') annotation and silently route queries to the default datasource instead of the specified connection. This affects @Where-annotated methods and all methods implemented via AbstractDetachedCriteriaServiceImplementor (including count(), list(), findAll*(), findOneBy*(), etc.).
The save() method is NOT affected because AbstractSaveImplementer correctly uses findConnectionId(abstractMethodNode).
Root Cause
Two implementers call findConnectionId(newMethodNode) instead of findConnectionId(abstractMethodNode):
-
AbstractWhereImplementer.groovyline 99:Expression connectionId = findConnectionId(newMethodNode) // BUG
-
AbstractDetachedCriteriaServiceImplementor.groovyline 78:Expression connectionId = findConnectionId(newMethodNode) // BUG
findConnectionId resolves the @Transactional annotation via TransactionalTransform.findTransactionalAnnotation(methodNode), which falls back to methodNode.getDeclaringClass(). The newMethodNode (the generated implementation method) declares on the generated $ServiceImplementation class, which does NOT carry the @Transactional annotation. The abstractMethodNode (the original interface/abstract class method) declares on the class that has the annotation.
This causes findConnectionId to return null, so the withConnection(connectionId) call on the DetachedCriteria is never added, and queries run against the default datasource.
Affected Code
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovyline 99grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovyline 78
Affected Data Service Methods
Any Data Service method that goes through AbstractDetachedCriteriaServiceImplementor or AbstractWhereImplementer:
@Where-annotated methodscount()/countBy*()list()/find()/findAll*()findBy*()/findOneBy*()- Any method returning domain class iterables
NOT affected: save() (uses AbstractSaveImplementer which correctly uses abstractMethodNode)
Example
@Service(Metric)
@Transactional(connection = 'secondary')
interface MetricDataService {
Metric save(String name, Double amount) // WORKS - saves to secondary
@Where({ amount >= minAmount })
List<Metric> findByMinAmount(Double minAmount) // BUG - queries default
}findByMinAmount() will query the default datasource instead of secondary.
Reproducer
https://github.com/jamesfredley/grails-15416-where-connection-routing
git clone https://github.com/jamesfredley/grails-15416-where-connection-routing.git
cd grails-15416-where-connection-routing
./gradlew integrationTestThe test passes by asserting the buggy behavior: the @Where query returns data from the default datasource instead of secondary. See inline comments in the test for what correct behavior should be.
Fix
Change findConnectionId(newMethodNode) to findConnectionId(abstractMethodNode) in both files:
AbstractWhereImplementer.groovy line 99:
// Before (bug):
Expression connectionId = findConnectionId(newMethodNode)
// After (fix):
Expression connectionId = findConnectionId(abstractMethodNode)AbstractDetachedCriteriaServiceImplementor.groovy line 78:
// Before (bug):
Expression connectionId = findConnectionId(newMethodNode)
// After (fix):
Expression connectionId = findConnectionId(abstractMethodNode)Both abstractMethodNode and newMethodNode are available in the doImplement method signature.
Context
Discovered while fixing a similar bug pattern in PR #15395. That PR introduced findConnectionId calls in several other implementers (SaveImplementer, DeleteImplementer, FindAndDeleteImplementer) which correctly use abstractMethodNode. The two implementers listed above pre-existed on the 7.0.x branch with the incorrect newMethodNode usage.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status