This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch feat/data-service-datasource-inheritance
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to 
refs/heads/feat/data-service-datasource-inheritance by this push:
     new 19205e6657 fix: generate connection-aware getTransactionManager() in 
ServiceTransformation for inherited datasource services
19205e6657 is described below

commit 19205e665791d6ba924e085973b92ca98979c140
Author: James Fredley <[email protected]>
AuthorDate: Mon Feb 23 23:00:23 2026 -0500

    fix: generate connection-aware getTransactionManager() in 
ServiceTransformation for inherited datasource services
    
    When a @Service data service auto-inherits its datasource from the domain 
class mapping,
    ServiceTransformation creates a synthetic impl class and adds 
@Transactional(connection='secondary')
    to it. However, TransactionalTransform runs in SEMANTIC_ANALYSIS phase 
before the impl class
    exists, so it never weaves getTransactionManager() with the correct 
connection-aware logic onto
    that impl. At runtime this caused 'No Session found for current thread' 
because the default
    transaction manager was used instead of the secondary one.
    
    Fix: ServiceTransformation now directly generates the 
getTransactionManager() method on the
    impl class in applyDomainConnectionToService(), mirroring exactly what 
TransactionalTransform
    would have done for Service-implementing classes with a connection 
attribute.
    
    Assisted-by: Claude Code <[email protected]>
---
 .../transform/ServiceTransformation.groovy         | 58 +++++++++++++++++++++-
 1 file changed, 56 insertions(+), 2 deletions(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/transform/ServiceTransformation.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/transform/ServiceTransformation.groovy
index d5d7c37812..8e7da22ee2 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/transform/ServiceTransformation.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/transform/ServiceTransformation.groovy
@@ -89,6 +89,10 @@ import 
org.grails.datastore.gorm.services.implementers.SaveImplementer
 import org.grails.datastore.gorm.services.implementers.UpdateOneImplementer
 import 
org.grails.datastore.gorm.services.implementers.UpdateStringQueryImplementer
 import org.grails.datastore.gorm.transactions.transform.TransactionalTransform
+import org.grails.datastore.gorm.GormEnhancer
+import 
org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
+import org.springframework.transaction.PlatformTransactionManager
 import 
org.grails.datastore.gorm.transform.AbstractTraitApplyingGormASTTransformation
 import 
org.grails.datastore.gorm.validation.jakarta.services.implementers.MethodValidationImplementer
 import org.grails.datastore.mapping.core.Datastore
@@ -96,14 +100,20 @@ import 
org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.order.OrderedComparator
 
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args
 import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.block
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.classX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS
+import static org.codehaus.groovy.ast.tools.GeneralUtils.notNullX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.param
 import static org.codehaus.groovy.ast.tools.GeneralUtils.params
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
+import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callD
 import static org.grails.datastore.mapping.reflect.AstUtils.COMPILE_STATIC_TYPE
 import static org.grails.datastore.mapping.reflect.AstUtils.ZERO_PARAMETERS
 import static 
org.grails.datastore.mapping.reflect.AstUtils.addAnnotationIfNecessary
@@ -531,7 +541,6 @@ class ServiceTransformation extends 
AbstractTraitApplyingGormASTTransformation i
 
     private static void applyDomainConnectionToService(ClassNode classNode, 
ClassNode implClass, String connectionName) {
         ConstantExpression connectionExpr = new 
ConstantExpression(connectionName)
-
         AnnotationNode classAnn = findAnnotation(classNode, Transactional)
         if (classAnn != null) {
             classAnn.setMember('connection', connectionExpr)
@@ -541,7 +550,6 @@ class ServiceTransformation extends 
AbstractTraitApplyingGormASTTransformation i
             newAnn.setMember('connection', connectionExpr)
             classNode.addAnnotation(newAnn)
         }
-
         AnnotationNode implAnn = findAnnotation(implClass, Transactional)
         if (implAnn != null) {
             implAnn.setMember('connection', connectionExpr)
@@ -551,6 +559,52 @@ class ServiceTransformation extends 
AbstractTraitApplyingGormASTTransformation i
             newImplAnn.setMember('connection', connectionExpr)
             implClass.addAnnotation(newImplAnn)
         }
+
+        // TransactionalTransform runs before ServiceTransformation creates 
the impl class, so it never
+        // gets a chance to weave getTransactionManager() with the correct 
connection-aware logic.
+        // We generate it here directly to ensure the right transaction 
manager is used at runtime.
+        generateConnectionAwareTransactionManager(implClass, connectionExpr)
+    }
+
+    /**
+     * Generates a {@code getTransactionManager()} method on the impl class 
that resolves the
+     * transaction manager for the inherited connection source. This mirrors 
the logic in
+     * {@link 
org.grails.datastore.gorm.transactions.transform.TransactionalTransform#weaveTransactionManagerAware}
+     * for classes implementing the {@link 
org.grails.datastore.mapping.services.Service} trait.
+     */
+    private static void generateConnectionAwareTransactionManager(ClassNode 
implClass, ConstantExpression connectionExpr) {
+        // Remove any existing getTransactionManager() that was added without 
connection awareness
+        implClass.getMethods('getTransactionManager').each { MethodNode 
existing ->
+            implClass.removeMethod(existing)
+        }
+
+        ClassNode transactionManagerClassNode = 
ClassHelper.make(PlatformTransactionManager)
+        ClassNode transactionCapableDatastore = 
ClassHelper.make(TransactionCapableDatastore)
+        ClassNode multipleConnectionDatastore = 
ClassHelper.make(MultipleConnectionSourceCapableDatastore)
+        ClassExpression gormEnhancerExpr = classX(GormEnhancer)
+
+        // datastore variable (field from Service trait)
+        VariableExpression datastoreVar = varX('datastore')
+        // ((MultipleConnectionSourceCapableDatastore) 
datastore).getDatastoreForConnection(connectionName)
+        Expression datastoreForConnection = 
callD(castX(multipleConnectionDatastore, datastoreVar), 
'getDatastoreForConnection', connectionExpr)
+        // .getTransactionManager()
+        Expression datastoreTxManager = 
propX(castX(transactionCapableDatastore, datastoreForConnection), 
'transactionManager')
+        // GormEnhancer.findSingleTransactionManager(connectionName)
+        Expression fallbackTxManager = callX(gormEnhancerExpr, 
'findSingleTransactionManager', args(connectionExpr))
+
+        // if (datastore != null) { return <datastoreTxManager> } else { 
return <fallbackTxManager> }
+        Statement body = ifElseS(
+                notNullX(datastoreVar),
+                returnS(datastoreTxManager),
+                returnS(fallbackTxManager)
+        )
+
+        MethodNode methodNode = implClass.addMethod('getTransactionManager',
+                Modifier.PUBLIC,
+                transactionManagerClassNode,
+                ZERO_PARAMETERS, null,
+                body)
+        markAsGenerated(implClass, methodNode)
     }
 
     @Override

Reply via email to