Fix @Where and DetachedCriteria query methods ignoring @Transactional(connection)#15418
Fix @Where and DetachedCriteria query methods ignoring @Transactional(connection)#15418jamesfredley wants to merge 1 commit into7.0.xfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes datasource connection routing for GORM Data Service query implementations so that class/interface-level @Transactional(connection = ...) is respected when generating @Where-based and DetachedCriteria-based query methods (e.g., count(), list(), dynamic findBy*() methods).
Changes:
- Resolve the connection id from the original service method (
abstractMethodNode) instead of the generated implementation method (newMethodNode) in@WhereandDetachedCriteriaimplementers. - Reorder
DetachedCriteria.build()vswithConnection()in the@Wherepath to avoid losing the connection due tobuild()cloning. - Add unit + integration regression tests covering connection routing for
@Whereand commonDetachedCriteria-backed query methods in multi-datasource scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy | Fix connection resolution node + ensure build() happens before withConnection() so connection isn’t dropped during cloning. |
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy | Fix connection resolution node for DetachedCriteria query implementers so class/interface @Transactional(connection) is honored. |
| grails-datamapping-core/src/test/groovy/grails/gorm/services/WhereConnectionRoutingSpec.groovy | Adds unit-level compilation/transform assertions for @Where and DetachedCriteria-implemented methods under @Transactional(connection). |
| grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy | Adds integration coverage verifying runtime query routing against separate default + secondary H2 datasources. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
grails-datamapping-core/src/test/groovy/grails/gorm/services/WhereConnectionRoutingSpec.groovy
Outdated
Show resolved
Hide resolved
|
The suggestion is valid. The unit spec tests @where method routing but lacks a findByName test, while the integration spec covers it. Adjust the PR description to clarify that integration tests verify DetachedCriteria findByName routing. |
…connection) Data Service methods using @where annotations or DetachedCriteria-based queries (count, list, findBy*) were ignoring the class-level @transactional(connection) annotation, causing queries to execute against the default datasource instead of the specified connection. Root cause: Both AbstractWhereImplementer and AbstractDetachedCriteriaServiceImplementor called findConnectionId(newMethodNode) instead of findConnectionId(abstractMethodNode). The newMethodNode belongs to the generated $ServiceImplementation class which lacks the @transactional annotation, while abstractMethodNode belongs to the original interface/abstract class where the annotation is declared. Additionally, in AbstractWhereImplementer the build() call was placed after withConnection(), but DetachedCriteria.clone() (called internally by build) does not copy the connectionName field, causing the connection setting to be lost. Fixed by reordering build() before withConnection(). Fixes #15416 Assisted-by: Claude Code <Claude@Claude.ai>
b461694 to
7ae3260
Compare
jdaugherty
left a comment
There was a problem hiding this comment.
The change is great, but what do you think about adding a test to the tck?
| void setup() { | ||
| def api = GormEnhancer.findStaticApi(Item, 'secondary') | ||
| api.withNewTransaction { | ||
| api.executeUpdate('delete from Item') |
There was a problem hiding this comment.
Is this from test pollution? Why isn't this in cleanup?
| ) | ||
| Expression connectionId = findConnectionId(newMethodNode) | ||
| body.addStatement( | ||
| assignS(queryVar, callX(queryVar, 'build', closureExpression)) |
There was a problem hiding this comment.
These changes are in core, but there are additional tests in hibernate. That strongly tells me the test should be part of the TCK.
Summary
Fixes #15416
Data Service methods using
@Whereannotations orDetachedCriteria-based queries (count(),list(),findBy*()) were ignoring the class-level@Transactional(connection)annotation, causing queries to execute against the default datasource instead of the specified connection.Reproducer: https://github.com/jamesfredley/grails-15416-where-connection-routing
Root Cause
Two issues were found:
1. Wrong method node passed to
findConnectionId()Both
AbstractWhereImplementerandAbstractDetachedCriteriaServiceImplementorcalledfindConnectionId(newMethodNode)instead offindConnectionId(abstractMethodNode).newMethodNodebelongs to the generated$ServiceImplementationclass, which does not carry the@TransactionalannotationabstractMethodNodebelongs to the original interface/abstract class where@Transactional(connection)is declaredfindConnectionId()resolves the connection by walking up tomethodNode.getDeclaringClass()to find@Transactional- so passing the wrong node meant it could never find the annotationThis is the same class of bug fixed in #15395 for
SaveImplementer,DeleteImplementer, andFindAndDeleteImplementer.2.
build()afterwithConnection()inAbstractWhereImplementerIn the
@Wherecode path, the original order was:query = new DetachedCriteria(Foo)query = query.withConnection('secondary')- setsconnectionNamequery = query.build(closure)- internally callsclone(), which does not copyconnectionNameThe
clone()call insidebuild()creates a newDetachedCriteriainstance without the connection setting, so the connection was silently lost.Fixed by reordering to:
new DetachedCriteria->build(closure)->withConnection(name).Changes
AbstractWhereImplementer.groovyfindConnectionId(newMethodNode)->findConnectionId(abstractMethodNode)+ reorderbuild()/withConnection()AbstractDetachedCriteriaServiceImplementor.groovyfindConnectionId(newMethodNode)->findConnectionId(abstractMethodNode)Tests
Unit Tests (
WhereConnectionRoutingSpec) - 5 tests@Wheremethod generateswithConnection()call when@Transactional(connection)is present@Wheremethod does NOT generatewithConnection()when no connection specifiedDetachedCriteriacount()generateswithConnection()callDetachedCriterialist()generateswithConnection()callDetachedCriteriafindByName()generateswithConnection()callIntegration Tests (
WhereQueryMultiDataSourceSpec) - 5 testsdatasource 'ALL'@Where-annotated method returns data from secondary@Where-annotated method returns empty from default (proving isolation)count()routes to secondarylist()routes to secondaryfindByName()routes to secondaryAll existing
ServiceTransformSpectests (30) continue to pass with no regressions.