You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/06/27 16:35:31 UTC

(grails-core) branch feat/gorm-registry-core-impl created (now e0e18f36b0)

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

borinquenkid pushed a change to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git


      at e0e18f36b0 style: remove unused ConnectionSource import and fix consecutive blank lines in HibernateGormEnhancer

This branch includes the following new commits:

     new 3901900ab1 feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor
     new fd1898e11e fix: restore registerConstraints to GormEnhancer dropped in scaling refactor
     new eff936d468 fix: restore registerConstraints hook and fix MongoStaticApi fields dropped by scaling refactor
     new ff165b4e4d fix: distinguish datasource connection qualifier from tenant ID in AbstractGormApi.execute()
     new 3067eb076d fix: mode-aware tenant lookup and bound DatastoreResolvers in GormRegistry
     new 7de58bbf71 fix: register HibernateGormApiFactory in registerConstraints; update affected tests
     new e0e18f36b0 style: remove unused ConnectionSource import and fix consecutive blank lines in HibernateGormEnhancer

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(grails-core) 03/07: fix: restore registerConstraints hook and fix MongoStaticApi fields dropped by scaling refactor

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit eff936d46851295b8b709faef6badb9a15ef4ed3
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Fri Jun 26 22:50:32 2026 -0500

    fix: restore registerConstraints hook and fix MongoStaticApi fields dropped by scaling refactor
    
    GormEnhancer: restore protected registerConstraints(Datastore) hook that H5/H7
    HibernateGormEnhancer override as a no-op. Its absence broke Java stub
    generation with "@Override … method does not override a supertype method".
    
    MongoStaticApi: restore persistentEntity and multiTenancyMode fields that
    GormStaticApi no longer carries after the scaling refactor. Initialise
    persistentEntity from the mapping context and multiTenancyMode from
    MongoDatastore.getMultiTenancyMode() so wrapFilterWithMultiTenancy and
    preparePipeline compile under @CompileStatic.
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy     | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy
index f460cfb2c2..762d6478b9 100644
--- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy
+++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoStaticApi.groovy
@@ -48,6 +48,7 @@ import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.core.Session
 import org.grails.datastore.mapping.engine.EntityPersister
 import org.grails.datastore.mapping.engine.internal.MappingUtils
+import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.datastore.mapping.mongo.AbstractMongoSession
 import org.grails.datastore.mapping.mongo.MongoCodecSession
 import org.grails.datastore.mapping.mongo.MongoDatastore
@@ -63,8 +64,15 @@ import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
 @CompileStatic
 class MongoStaticApi<D> extends GormStaticApi<D> implements MongoAllOperations<D> {
 
+    protected final PersistentEntity persistentEntity
+    protected final MultiTenancySettings.MultiTenancyMode multiTenancyMode
+
     MongoStaticApi(Class<D> persistentClass, Datastore datastore, List<FinderMethod> finders, PlatformTransactionManager transactionManager) {
         super(persistentClass, datastore, finders, transactionManager)
+        this.persistentEntity = datastore.mappingContext.getPersistentEntity(persistentClass.name)
+        this.multiTenancyMode = (datastore instanceof MongoDatastore)
+                ? ((MongoDatastore) datastore).multiTenancyMode
+                : MultiTenancySettings.MultiTenancyMode.NONE
     }
 
     FindIterable<D> find(Bson filter) {


(grails-core) 06/07: fix: register HibernateGormApiFactory in registerConstraints; update affected tests

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 7de58bbf71653e8434af7c739157969bf9faa794
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jun 27 10:43:59 2026 -0500

    fix: register HibernateGormApiFactory in registerConstraints; update affected tests
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../manager/GraphQLDataFetcherManagerSpec.groovy   |  5 +-
 .../orm/hibernate/HibernateGormApiFactory.groovy   | 73 ++++++++++++++++++++++
 .../orm/hibernate/HibernateGormEnhancer.groovy     |  5 +-
 .../core/GrailsDataHibernate5TckManager.groovy     | 19 +++---
 .../datastore/gorm/GormEnhancerCleanupSpec.groovy  | 61 ++++++------------
 .../orm/hibernate/HibernateGormApiFactory.groovy   | 73 ++++++++++++++++++++++
 .../orm/hibernate/HibernateGormEnhancer.groovy     |  4 +-
 .../datastore/gorm/GormEnhancerCleanupSpec.groovy  | 63 ++++++-------------
 .../hibernate/HibernateGormInstanceApiSpec.groovy  | 10 +--
 .../hibernate/HibernateGormStaticApiSpec.groovy    |  3 +-
 .../HibernateGormValidationApiSpec.groovy          |  5 +-
 .../hibernate/SchemaTenantGormEnhancerSpec.groovy  |  2 -
 .../GrailsIdentityGeneratorSpec.groovy             |  5 ++
 .../testing/tck/base/GrailsDataTckManager.groovy   | 14 +++--
 14 files changed, 226 insertions(+), 116 deletions(-)

diff --git a/grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/fetcher/manager/GraphQLDataFetcherManagerSpec.groovy b/grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/fetcher/manager/GraphQLDataFetcherManagerSpec.groovy
index 0031474e1d..9ff4b35d73 100644
--- a/grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/fetcher/manager/GraphQLDataFetcherManagerSpec.groovy
+++ b/grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/fetcher/manager/GraphQLDataFetcherManagerSpec.groovy
@@ -20,9 +20,8 @@
 package org.grails.gorm.graphql.fetcher.manager
 
 import graphql.schema.DataFetchingEnvironment
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
-import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.gorm.graphql.binding.GraphQLDataBinder
 import org.grails.gorm.graphql.fetcher.BindingGormDataFetcher
@@ -157,7 +156,7 @@ class GraphQLDataFetcherManagerSpec extends Specification {
 
     void "test registering a binding fetcher"() {
         given:
-        GormEnhancer.STATIC_APIS.put(ConnectionSource.DEFAULT, ['java.lang.String': Mock(GormStaticApi)])
+        GormRegistry.instance.staticApiRegistry.register('java.lang.String', Mock(GormStaticApi))
 
         when:
         manager.registerBindingDataFetcher(String, mockBindingFetcher)
diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
new file mode 100644
index 0000000000..9a89310cec
--- /dev/null
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
@@ -0,0 +1,73 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.DefaultGormApiFactory
+import org.grails.datastore.gorm.GormApiFactory
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * {@link GormApiFactory} that produces Hibernate 5-specific API instances, ensuring that
+ * {@code withNewSession} and related lifecycle methods use the Hibernate {@code SessionFactory}
+ * binding rather than the generic GORM {@code DatastoreUtils} path.
+ */
+@CompileStatic
+class HibernateGormApiFactory implements GormApiFactory {
+
+    private final ClassLoader classLoader
+
+    HibernateGormApiFactory(ClassLoader classLoader) {
+        this.classLoader = classLoader
+    }
+
+    @Override
+    <D> GormStaticApi<D> createStaticApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, String qualifier, GormRegistry registry) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        List<FinderMethod> finders = new DefaultGormApiFactory().createDynamicFinders(resolver, mappingContext)
+        return new HibernateGormStaticApi<D>(persistentClass, hds, finders, classLoader, hds.getTransactionManager())
+    }
+
+    @Override
+    <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry, boolean failOnError, boolean markDirty) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        GormInstanceApi<D> instanceApi = new HibernateGormInstanceApi<D>(persistentClass, hds, classLoader)
+        instanceApi.failOnError = failOnError
+        instanceApi.markDirty = markDirty
+        return instanceApi
+    }
+
+    @Override
+    <D> GormValidationApi<D> createValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        return new HibernateGormValidationApi<D>(persistentClass, hds, classLoader)
+    }
+
+    @Override
+    List<FinderMethod> createDynamicFinders(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        new DefaultGormApiFactory().createDynamicFinders(datastoreResolver, mappingContext)
+    }
+}
diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
index 9a47fb8c41..7b2f8f83b1 100644
--- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
@@ -24,9 +24,11 @@ import org.springframework.transaction.PlatformTransactionManager
 
 import org.grails.datastore.gorm.GormEnhancer
 import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
 import org.grails.datastore.gorm.GormValidationApi
 import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
 
 /**
@@ -48,6 +50,7 @@ class HibernateGormEnhancer extends GormEnhancer {
         super(datastore, transactionManager, settings)
     }
 
+
     @Override
     protected <D> GormStaticApi<D> getStaticApi(Class<D> cls, String qualifier) {
         HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore
@@ -75,6 +78,6 @@ class HibernateGormEnhancer extends GormEnhancer {
 
     @Override
     protected void registerConstraints(Datastore datastore) {
-        // no-op
+        GormRegistry.instance.registerApiFactory(HibernateDatastore, new HibernateGormApiFactory(Thread.currentThread().contextClassLoader))
     }
 }
diff --git a/grails-data-hibernate5/core/src/test/groovy/org/apache/grails/data/hibernate5/core/GrailsDataHibernate5TckManager.groovy b/grails-data-hibernate5/core/src/test/groovy/org/apache/grails/data/hibernate5/core/GrailsDataHibernate5TckManager.groovy
index b6a730ae32..74c435eba9 100644
--- a/grails-data-hibernate5/core/src/test/groovy/org/apache/grails/data/hibernate5/core/GrailsDataHibernate5TckManager.groovy
+++ b/grails-data-hibernate5/core/src/test/groovy/org/apache/grails/data/hibernate5/core/GrailsDataHibernate5TckManager.groovy
@@ -53,7 +53,8 @@ class GrailsDataHibernate5TckManager extends GrailsDataTckManager {
     ApplicationContext applicationContext
     HibernateDatastore multiDataSourceDatastore
     HibernateDatastore multiTenantMultiDataSourceDatastore
-    Map grailsConfig
+    ConfigObject grailsConfig = new ConfigObject()
+    boolean isTransactional = true
 
     @Override
     void setup(Class<? extends Specification> spec) {
@@ -63,20 +64,14 @@ class GrailsDataHibernate5TckManager extends GrailsDataTckManager {
 
     @Override
     Session createSession() {
-        ConfigObject config = new ConfigObject()
+        System.setProperty('hibernate5.gorm.suite', "true")
+        grailsConfig.dataSource.dbCreate = grailsConfig.dataSource.dbCreate ?: "create-drop"
+        grailsApplication = new DefaultGrailsApplication(domainClasses as Class[], new GroovyClassLoader(GrailsDataHibernate5TckManager.getClassLoader()))
         if (grailsConfig) {
-            config.putAll(grailsConfig)
-        }
-        if (!config.containsKey('dataSource.dbCreate') && !config.dataSource.containsKey('dbCreate')) {
-            config.dataSource.dbCreate = "create-drop"
+            grailsApplication.config.putAll(grailsConfig)
         }
-        boolean isTransactional = true
-
-        System.setProperty('hibernate5.gorm.suite', "true")
-        grailsApplication = new DefaultGrailsApplication(domainClasses, new GroovyClassLoader(GrailsDataHibernate5TckManager.getClassLoader()))
-        grailsApplication.config.putAll(config)
 
-        hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), domainClasses)
+        hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(grailsConfig), domainClasses as Class[])
         transactionManager = hibernateDatastore.getTransactionManager()
         sessionFactory = hibernateDatastore.sessionFactory
         if (transactionStatus == null && isTransactional) {
diff --git a/grails-data-hibernate5/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
index 9686265a1c..aa382c9697 100644
--- a/grails-data-hibernate5/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
+++ b/grails-data-hibernate5/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
@@ -20,7 +20,7 @@ package org.grails.datastore.gorm
 
 import grails.gorm.annotation.Entity
 import grails.gorm.tests.HibernateGormDatastoreSpec
-import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 
 class GormEnhancerCleanupSpec extends HibernateGormDatastoreSpec {
 
@@ -28,56 +28,33 @@ class GormEnhancerCleanupSpec extends HibernateGormDatastoreSpec {
         manager.registerDomainClasses(CleanupEntity)
     }
 
-    void "Test that GormEnhancer.close() removes datastore from DATASTORES registry"() {
+    void "GormRegistry tracks entity-to-datastore mapping for registered entities"() {
         given:
-        def enhancerClass = GormEnhancer.class
-        def datastoresField = enhancerClass.getDeclaredField("DATASTORES")
-        datastoresField.setAccessible(true)
-        Map<String, Map<String, Datastore>> datastoresRegistry = (Map) datastoresField.get(null)
+        def registry = GormRegistry.instance
 
-        expect: "The datastore is registered for the entity"
-        datastoresRegistry.get("default")?.get(CleanupEntity.name) == datastore
+        expect: "CleanupEntity maps to the active datastore"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == datastore
 
-        when: "The datastore is closed"
-        datastore.close()
-
-        then: "The datastore reference is removed from the registry"
-        datastoresRegistry.get("default")?.get(CleanupEntity.name) == null
+        and: "A static API is registered for CleanupEntity"
+        registry.getStaticApi(CleanupEntity) != null
     }
 
-    void "Test that GormEnhancer.close() does not mutate maps via withDefault"() {
+    void "GormRegistry.removeDatastore clears entity and static API entries"() {
         given:
-        def enhancerClass = GormEnhancer.class
-        def staticApisField = enhancerClass.getDeclaredField("STATIC_APIS")
-        staticApisField.setAccessible(true)
-        Map staticApisRegistry = (Map) staticApisField.get(null)
+        def registry = GormRegistry.instance
+
+        expect: "entries are present before removal"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == datastore
+        registry.getStaticApi(CleanupEntity) != null
 
-        String unknownQualifier = "unknown_tenant_" + System.currentTimeMillis()
-        
-        expect: "The unknown qualifier is not in the map"
-        !staticApisRegistry.containsKey(unknownQualifier)
+        when: "the datastore is removed from the registry"
+        registry.removeDatastore(datastore)
 
-        when: "Closing a datastore with an unknown qualifier (simulated)"
-        // This is tricky because we need a datastore that 'claims' to have this qualifier
-        // We'll just manually call close() with a mock/stub if possible, 
-        // but GormEnhancer uses 'this.datastore' internally.
-        
-        // Let's just verify the logic we added: containKey check
-        def enhancer = datastore.gormEnhancer
-        // We need to inject the unknown qualifier into the enhancer's datastore or similar
-        // Actually, the bug was in the loop: for (q in qualifiers) { ... STATIC_APIS.get(q) ... }
-        // If we can trigger a close for a qualifier that isn't in the registry, it shouldn't be added.
-        
-        // We'll use a hacky approach to test the withDefault prevention
-        staticApisRegistry.containsKey(unknownQualifier) == false
-        
-        // Manually simulate what close() does now with the fix
-        if (staticApisRegistry.containsKey(unknownQualifier)) {
-             staticApisRegistry.get(unknownQualifier).remove("SomeClass")
-        }
+        then: "entity-to-datastore mapping is cleared"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == null
 
-        then: "The qualifier was NOT added to the map"
-        !staticApisRegistry.containsKey(unknownQualifier)
+        and: "static API is also cleared"
+        registry.getStaticApi(CleanupEntity) == null
     }
 }
 
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
new file mode 100644
index 0000000000..03bc49d139
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
@@ -0,0 +1,73 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.DefaultGormApiFactory
+import org.grails.datastore.gorm.GormApiFactory
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * {@link GormApiFactory} that produces Hibernate 7-specific API instances, ensuring that
+ * {@code withNewSession} and related lifecycle methods use the Hibernate {@code SessionFactory}
+ * binding rather than the generic GORM {@code DatastoreUtils} path.
+ */
+@CompileStatic
+class HibernateGormApiFactory implements GormApiFactory {
+
+    private final ClassLoader classLoader
+
+    HibernateGormApiFactory(ClassLoader classLoader) {
+        this.classLoader = classLoader
+    }
+
+    @Override
+    <D> GormStaticApi<D> createStaticApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, String qualifier, GormRegistry registry) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        List<FinderMethod> finders = new DefaultGormApiFactory().createDynamicFinders(resolver, mappingContext)
+        return new HibernateGormStaticApi<D>(persistentClass, hds, finders, classLoader, hds.getTransactionManager(), qualifier)
+    }
+
+    @Override
+    <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry, boolean failOnError, boolean markDirty) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        GormInstanceApi<D> instanceApi = new HibernateGormInstanceApi<D>(persistentClass, hds, classLoader)
+        instanceApi.failOnError = failOnError
+        instanceApi.markDirty = markDirty
+        return instanceApi
+    }
+
+    @Override
+    <D> GormValidationApi<D> createValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry) {
+        HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
+        return new HibernateGormValidationApi<D>(persistentClass, hds, classLoader)
+    }
+
+    @Override
+    List<FinderMethod> createDynamicFinders(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        new DefaultGormApiFactory().createDynamicFinders(datastoreResolver, mappingContext)
+    }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
index 1349ae24f6..29e4e5b256 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
@@ -38,9 +38,11 @@ import org.springframework.transaction.PlatformTransactionManager
 
 import org.grails.datastore.gorm.GormEnhancer
 import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
 import org.grails.datastore.gorm.GormValidationApi
 import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
 
 /**
@@ -90,7 +92,7 @@ class HibernateGormEnhancer extends GormEnhancer {
 
     @Override
     protected void registerConstraints(Datastore datastore) {
-        // no-op
+        GormRegistry.instance.registerApiFactory(HibernateDatastore, new HibernateGormApiFactory(Thread.currentThread().contextClassLoader))
     }
 
     public static <D> GormStaticApi<D> findStaticApi(Class<D> cls, String qualifier) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
index 6874a037e6..fffe874656 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
@@ -16,9 +16,7 @@ package org.grails.datastore.gorm
 
 import grails.gorm.annotation.Entity
 import grails.gorm.tests.HibernateGormDatastoreSpec
-import org.grails.datastore.mapping.core.Datastore
-import spock.lang.Specification
-import java.util.concurrent.ConcurrentHashMap
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 
 class GormEnhancerCleanupSpec extends HibernateGormDatastoreSpec {
 
@@ -26,56 +24,33 @@ class GormEnhancerCleanupSpec extends HibernateGormDatastoreSpec {
         manager.registerDomainClasses(CleanupEntity)
     }
 
-    void "Test that GormEnhancer.close() removes datastore from DATASTORES registry"() {
+    void "GormRegistry tracks entity-to-datastore mapping for registered entities"() {
         given:
-        def enhancerClass = GormEnhancer.class
-        def datastoresField = enhancerClass.getDeclaredField("DATASTORES")
-        datastoresField.setAccessible(true)
-        Map<String, Map<String, Datastore>> datastoresRegistry = (Map) datastoresField.get(null)
+        def registry = GormRegistry.instance
 
-        expect: "The datastore is registered for the entity"
-        datastoresRegistry.get("default")?.get(CleanupEntity.name) == datastore
+        expect: "CleanupEntity maps to the active datastore"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == datastore
 
-        when: "The datastore is closed"
-        datastore.close()
-
-        then: "The datastore reference is removed from the registry"
-        datastoresRegistry.get("default")?.get(CleanupEntity.name) == null
+        and: "A static API is registered for CleanupEntity"
+        registry.getStaticApi(CleanupEntity) != null
     }
 
-    void "Test that GormEnhancer.close() does not mutate maps via withDefault"() {
+    void "GormRegistry.removeDatastore clears entity and static API entries"() {
         given:
-        def enhancerClass = GormEnhancer.class
-        def staticApisField = enhancerClass.getDeclaredField("STATIC_APIS")
-        staticApisField.setAccessible(true)
-        Map staticApisRegistry = (Map) staticApisField.get(null)
+        def registry = GormRegistry.instance
+
+        expect: "entries are present before removal"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == datastore
+        registry.getStaticApi(CleanupEntity) != null
 
-        String unknownQualifier = "unknown_tenant_" + System.currentTimeMillis()
-        
-        expect: "The unknown qualifier is not in the map"
-        !staticApisRegistry.containsKey(unknownQualifier)
+        when: "the datastore is removed from the registry"
+        registry.removeDatastore(datastore)
 
-        when: "Closing a datastore with an unknown qualifier (simulated)"
-        // This is tricky because we need a datastore that 'claims' to have this qualifier
-        // We'll just manually call close() with a mock/stub if possible, 
-        // but GormEnhancer uses 'this.datastore' internally.
-        
-        // Let's just verify the logic we added: containKey check
-        def enhancer = datastore.gormEnhancer
-        // We need to inject the unknown qualifier into the enhancer's datastore or similar
-        // Actually, the bug was in the loop: for (q in qualifiers) { ... STATIC_APIS.get(q) ... }
-        // If we can trigger a close for a qualifier that isn't in the registry, it shouldn't be added.
-        
-        // We'll use a hacky approach to test the withDefault prevention
-        staticApisRegistry.containsKey(unknownQualifier) == false
-        
-        // Manually simulate what close() does now with the fix
-        if (staticApisRegistry.containsKey(unknownQualifier)) {
-             staticApisRegistry.get(unknownQualifier).remove("SomeClass")
-        }
+        then: "entity-to-datastore mapping is cleared"
+        registry.getDatastore(CleanupEntity, ConnectionSource.DEFAULT) == null
 
-        then: "The qualifier was NOT added to the map"
-        !staticApisRegistry.containsKey(unknownQualifier)
+        and: "static API is also cleared"
+        registry.getStaticApi(CleanupEntity) == null
     }
 }
 
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
index c1e3fd24fb..afad06923e 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
@@ -22,6 +22,8 @@ import grails.gorm.tests.HibernateGormDatastoreSpec
 import grails.gorm.annotation.Entity
 import grails.gorm.hibernate.HibernateEntity
 import grails.gorm.transactions.Rollback
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 
 import org.hibernate.FlushMode
 import org.grails.orm.hibernate.query.SelectHqlQuery
@@ -34,19 +36,19 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
 
     void "Test that HibernateGormInstanceApi uses the shared template from the datastore"() {
         given:
-        def enhancer = manager.hibernateDatastore.gormEnhancer
-        def api = enhancer.getInstanceApi(PersonInstanceApi)
+        def api = (HibernateGormInstanceApi) GormRegistry.instance.getInstanceApi(PersonInstanceApi.name)
 
         expect:
+        api instanceof HibernateGormInstanceApi
         api.hibernateTemplate.is(manager.hibernateDatastore.getHibernateTemplate())
     }
 
     void "Test that HibernateGormInstanceApi uses the shared InstanceApiHelper from the datastore"() {
         given:
-        def enhancer = manager.hibernateDatastore.gormEnhancer
-        def api = enhancer.getInstanceApi(PersonInstanceApi)
+        def api = (HibernateGormInstanceApi) GormRegistry.instance.getInstanceApi(PersonInstanceApi.name)
 
         expect:
+        api instanceof HibernateGormInstanceApi
         api.instanceApiHelper.is(manager.hibernateDatastore.getInstanceApiHelper())
     }
 
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
index 8b64a33847..13e995c66c 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
@@ -23,6 +23,7 @@ package org.grails.orm.hibernate
 import grails.gorm.tests.HibernateGormDatastoreSpec
 import grails.gorm.annotation.Entity
 import grails.gorm.tests.entities.Club
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.hibernate.jpa.AvailableHints
 
 class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
@@ -34,7 +35,7 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
     void "Test that HibernateGormStaticApi uses the shared template from the datastore"() {
         given:
         def enhancer = manager.hibernateDatastore.gormEnhancer
-        def api = enhancer.getStaticApi(HibernateGormStaticApiEntity)
+        def api = enhancer.getStaticApi(HibernateGormStaticApiEntity, ConnectionSource.DEFAULT)
 
         expect:
         api.hibernateTemplate.is(manager.hibernateDatastore.getHibernateTemplate())
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
index f1ddff8bc1..8c18ad838a 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
@@ -20,6 +20,7 @@ package org.grails.orm.hibernate
 
 import grails.gorm.annotation.Entity
 import grails.gorm.transactions.Rollback
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.orm.hibernate.cfg.Settings
 import org.springframework.core.env.PropertyResolver
@@ -39,10 +40,10 @@ class HibernateGormValidationApiSpec extends Specification {
 
     void "Test that HibernateGormValidationApi uses the shared template from the datastore"() {
         given:
-        def enhancer = hibernateDatastore.gormEnhancer
-        def api = enhancer.getValidationApi(ValidatedBook)
+        def api = (HibernateGormValidationApi) GormRegistry.instance.getValidationApi(ValidatedBook.name)
 
         expect:
+        api instanceof HibernateGormValidationApi
         api.hibernateTemplate.is(hibernateDatastore.getHibernateTemplate())
     }
 
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
index 4ac136e57f..4e52d357fe 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
@@ -22,7 +22,6 @@ import java.lang.reflect.Modifier
 
 import grails.gorm.MultiTenant
 import grails.gorm.annotation.Entity
-import grails.gorm.multitenancy.CurrentTenant
 import org.grails.datastore.gorm.GormEntity
 import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
@@ -162,7 +161,6 @@ class SchemaTenantGormEnhancerSpec extends Specification {
 }
 
 @Entity
-@CurrentTenant
 class SchemaTenantBook implements GormEntity<SchemaTenantBook>, MultiTenant<SchemaTenantBook> {
     String title
     static constraints = { title blank: false }
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy
index bc051b5d56..12325130e2 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy
@@ -33,6 +33,11 @@ import org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsIdentityGenera
 
 class GrailsIdentityGeneratorSpec extends HibernateGormDatastoreSpec {
 
+    @Override
+    void setupSpec() {
+        manager.registerDomainClasses(TestEntity, ChildEntity)
+    }
+
     def "should configure identity generator and set column as identity"() {
         given:
         def context = Mock(GeneratorCreationContext)
diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/base/GrailsDataTckManager.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/base/GrailsDataTckManager.groovy
index 4ba4d8f439..ade0126263 100644
--- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/base/GrailsDataTckManager.groovy
+++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/base/GrailsDataTckManager.groovy
@@ -31,8 +31,7 @@ abstract class GrailsDataTckManager {
 
     abstract Session createSession()
 
-    private Set<Class> domainClasses = [
-    ]
+    private Set<Class> domainClasses = []
 
     /**
      * Returns a defensive copy of the registered domain classes.
@@ -42,9 +41,16 @@ abstract class GrailsDataTckManager {
         domainClasses as Class[]
     }
 
+    @Deprecated
+    void addAllDomainClasses(Collection<Class> classes) {
+        if (classes) {
+            registerDomainClasses(classes as Class[])
+        }
+    }
+
     /**
-     * Registers the domain classes that will be available when testing.
-     * @param classes The classes to register
+     * Adds all the specified classes to the domain classes list.
+     * @param classes The classes to add
      */
     void registerDomainClasses(Class... classes) {
         if (classes) {


(grails-core) 07/07: style: remove unused ConnectionSource import and fix consecutive blank lines in HibernateGormEnhancer

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit e0e18f36b0ae3852d6a9e92cf63f67841ec9d6c7
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jun 27 11:20:53 2026 -0500

    style: remove unused ConnectionSource import and fix consecutive blank lines in HibernateGormEnhancer
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy   | 2 --
 .../main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy   | 1 -
 2 files changed, 3 deletions(-)

diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
index 7b2f8f83b1..7433f4ca9d 100644
--- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
@@ -28,7 +28,6 @@ import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
 import org.grails.datastore.gorm.GormValidationApi
 import org.grails.datastore.mapping.core.Datastore
-import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
 
 /**
@@ -50,7 +49,6 @@ class HibernateGormEnhancer extends GormEnhancer {
         super(datastore, transactionManager, settings)
     }
 
-
     @Override
     protected <D> GormStaticApi<D> getStaticApi(Class<D> cls, String qualifier) {
         HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
index 29e4e5b256..225bcb9f4a 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
@@ -42,7 +42,6 @@ import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
 import org.grails.datastore.gorm.GormValidationApi
 import org.grails.datastore.mapping.core.Datastore
-import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
 
 /**


(grails-core) 04/07: fix: distinguish datasource connection qualifier from tenant ID in AbstractGormApi.execute()

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit ff165b4e4d74566177eeb73663f324bf1c4519a4
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jun 27 09:41:43 2026 -0500

    fix: distinguish datasource connection qualifier from tenant ID in AbstractGormApi.execute()
    
    The GORM scaling commit introduced a non-default qualifier path in execute() that
    unconditionally called Tenants.withId(datastore, qualifier) for multi-tenant entities.
    This was correct for DATABASE mode (qualifier == tenant ID == connection name) but
    broke DISCRIMINATOR mode: when a @Service with @Transactional(connection='secondary')
    executed a query, 'secondary' was bound as the current tenant ID instead of the real
    tenant from the TenantResolver, causing discriminator filters to match 'secondary' and
    return 0 rows.
    
    Fix: probe getDatastoreForConnection(qualifier) to determine whether the qualifier
    names a real datasource connection. If it resolves (non-null), it is a connection name
    — fall through to executeQualified without touching the tenant context. If it throws or
    returns null, the qualifier is a tenant ID (e.g. from withTenant()) — bind it via
    Tenants.withId as before.
    
    Update GormRegistrySpec to explicitly stub getDatastoreForConnection(_) >> null on the
    DISCRIMINATOR-mode test stub, mirroring real HibernateDatastore behaviour (which throws
    ConfigurationException for unknown connection names) and avoiding Spock's covariant-
    interface default of returning the stub itself.
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../grails/datastore/gorm/AbstractGormApi.groovy   |  28 +-
 .../grails/datastore/gorm/GormRegistrySpec.groovy  | 491 +++++++++++++++++++++
 2 files changed, 516 insertions(+), 3 deletions(-)

diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
index a3bebc24cd..614f9302c1 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
@@ -35,6 +35,7 @@ import org.grails.datastore.mapping.core.Session
 import org.grails.datastore.mapping.core.SessionCallback
 import org.grails.datastore.mapping.core.VoidSessionCallback
 import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
 import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
@@ -101,9 +102,30 @@ abstract class AbstractGormApi<D> extends AbstractDatastoreApi {
         // Check if we have a non-default qualifier
         if (currentQualifier != null && !ConnectionSource.DEFAULT.equals(currentQualifier) && !ConnectionSource.OLD_DEFAULT.equalsIgnoreCase(currentQualifier)) {
             if (isMultiTenantEntity && isMultiTenantCapable) {
-                // If it's a multi-tenant entity and we have a qualifier, bind it as the tenant ID
-                return (T1) Tenants.withId((MultiTenantCapableDatastore)ds, (Serializable)currentQualifier) {
-                    DatastoreUtils.execute(ds, callback)
+                // Determine whether the qualifier names a datasource connection or is a tenant ID.
+                // A datasource connection qualifier resolves via getDatastoreForConnection(); a tenant ID
+                // (e.g. from withTenant("t1")) does not. When it IS a connection qualifier we must not
+                // bind it as the tenant ID — doing so overwrites the tenant context set by the
+                // TenantResolver (e.g. SystemPropertyTenantResolver) and causes discriminator filters to
+                // match the connection name instead of the real tenant.
+                boolean isConnectionQualifier = false
+                if (ds instanceof MultipleConnectionSourceCapableDatastore) {
+                    try {
+                        Datastore resolved = ((MultipleConnectionSourceCapableDatastore) ds)
+                                .getDatastoreForConnection(currentQualifier)
+                        if (resolved != null) {
+                            isConnectionQualifier = true
+                        }
+                    } catch (Exception ignored) {
+                        // qualifier is not a known datasource name; treat it as a tenant ID below
+                    }
+                }
+                if (!isConnectionQualifier) {
+                    // Qualifier is a tenant ID — bind it so the session and any discriminator filter
+                    // both see the correct tenant for this operation.
+                    return (T1) Tenants.withId((MultiTenantCapableDatastore)ds, (Serializable)currentQualifier) {
+                        DatastoreUtils.execute(ds, callback)
+                    }
                 }
             }
             return executeQualified(currentQualifier, callback)
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormRegistrySpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormRegistrySpec.groovy
new file mode 100644
index 0000000000..d4fc9298dd
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormRegistrySpec.groovy
@@ -0,0 +1,491 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  'License'); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import grails.gorm.multitenancy.Tenants
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.ConnectionSources
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
+import org.springframework.transaction.PlatformTransactionManager
+import spock.lang.Specification
+
+class GormRegistrySpec extends Specification {
+
+    void setup() {
+        GormRegistry.instance.reset()
+    }
+
+    void cleanup() {
+        GormRegistry.instance.reset()
+    }
+
+    void "reset clears all registries"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.reset()
+
+        then:
+        registry.allDatastores.isEmpty()
+    }
+
+    void "findSingleTransactionManager returns null for non-transactional datastore"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+
+        then:
+        registry.findSingleTransactionManager() == null
+    }
+
+    void "findSingleTransactionManager returns transaction manager for TransactionCapableDatastore"() {
+        given:
+        def txManager = Stub(PlatformTransactionManager)
+        def datastore = Stub(TransactionCapableDatastore) {
+            getTransactionManager() >> txManager
+        }
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+
+        then:
+        registry.findSingleTransactionManager() == txManager
+    }
+
+    void "findSingleTransactionManager with connectionName returns transaction manager"() {
+        given:
+        def txManager = Stub(PlatformTransactionManager)
+        def datastore = Stub(TransactionCapableDatastore) {
+            getTransactionManager() >> txManager
+        }
+        def registry = GormRegistry.instance
+
+        when:
+        registry.registerDatastore("ds1", datastore)
+
+        then:
+        registry.findSingleTransactionManager("ds1") == txManager
+    }
+
+    void "findTransactionManager returns transaction manager for entity"() {
+        given:
+        def txManager = Stub(PlatformTransactionManager)
+        def datastore = Stub(TransactionCapableDatastore) {
+            getTransactionManager() >> txManager
+        }
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        // Register entity datastore directly to avoid GormEnhancer complexity
+        registry.registerEntityDatastore(TestEntity.name, ConnectionSource.DEFAULT, datastore)
+
+        then:
+        registry.findTransactionManager(TestEntity) == txManager
+    }
+    
+    void "removeEntityDatastore removes datastore specifically for entity"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.registerEntityDatastore(TestEntity.name, ConnectionSource.DEFAULT, datastore)
+        registry.removeEntityDatastore(TestEntity.name, datastore)
+
+        then:
+        registry.getDatastore(TestEntity.name) == null
+    }
+
+    void "removeDatastoreByType removes from type registry but keeps in allDatastores"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.removeDatastoreByType(datastore.getClass())
+
+        then:
+        registry.allDatastores.contains(datastore)
+        !registry.datastoresByType.containsKey(datastore.getClass())
+    }
+
+    void "removeDatastoreFromDiscovery removes from type registry and allDatastores"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.removeDatastoreFromDiscovery(datastore)
+
+        then:
+        !registry.allDatastores.contains(datastore)
+        !registry.datastoresByType.containsKey(datastore.getClass())
+    }
+
+    void "removeDatastore removes from all registries"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.removeDatastore(datastore)
+
+        then:
+        registry.allDatastores.isEmpty()
+        registry.datastoresByQualifier.isEmpty()
+    }
+
+    void "normalizeEntityKey properly normalizes class names"() {
+        given:
+        def registry = GormRegistry.instance
+
+        expect:
+        registry.normalizeEntityKey(TestEntity) == TestEntity.name
+        registry.normalizeEntityKey(TestEntity.name) == TestEntity.name
+        registry.normalizeEntityKey(null) == null
+    }
+
+    void "normalizeQualifier properly normalizes qualifiers"() {
+        given:
+        def registry = GormRegistry.instance
+
+        expect:
+        registry.normalizeQualifier(null) == ConnectionSource.DEFAULT
+        registry.normalizeQualifier("") == ConnectionSource.DEFAULT
+        registry.normalizeQualifier("ds1") == "ds1"
+    }
+
+    void "registerDatastoreByQualifier only registers by qualifier"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.registerDatastoreByQualifier("ds1", datastore)
+
+        then:
+        registry.datastoresByQualifier.get("ds1") == datastore
+        !registry.allDatastores.contains(datastore)
+    }
+
+    void "getApiFactory falls back to parent type or default if specific type is not registered"() {
+        given:
+        def datastore1 = Stub(Datastore1)
+        def datastore2 = Stub(Datastore2)
+        def factory = Stub(GormApiFactory)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.registerApiFactory(Datastore1, factory)
+
+        then:
+        registry.getApiFactory(datastore1) == factory
+        registry.getApiFactory(datastore2) instanceof DefaultGormApiFactory
+    }
+
+    void "test withTenant and exists with multi-tenant entity in DISCRIMINATOR mode"() {
+        given:
+        def datastore = Stub(MixedDatastore) {
+            getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
+            getConnectionSources() >> Stub(ConnectionSources) {
+                getDefaultConnectionSource() >> Stub(ConnectionSource) {
+                    getName() >> "default"
+                }
+            }
+            // Spock stubs return themselves for covariant interface methods; explicit null here mirrors
+            // the real HibernateDatastore behavior where unknown connection names throw.
+            getDatastoreForConnection(_) >> null
+        }
+        def mappingContext = Stub(org.grails.datastore.mapping.model.MappingContext)
+        def entity = Stub(PersistentEntity) {
+            getName() >> "TestEntity"
+            getJavaClass() >> TestEntity
+            isMultiTenant() >> true
+            getMappingContext() >> mappingContext
+        }
+        
+        def registry = GormRegistry.instance
+        registry.registerDatastore("default", datastore)
+        
+        def staticApi = new GormStaticApi(TestEntity, mappingContext, [], new DatastoreResolver() {
+            @Override Datastore resolve() { return datastore }
+        }, ConnectionSource.DEFAULT, registry)
+        
+        TestEntity.metaClass.static.getGormPersistentEntity = { entity }
+        registry.registerApi(TestEntity.name, staticApi, null, null)
+
+        when: "Calling exists via withTenant"
+        def capturedTenantId = null
+        // Capture tenant ID during call to connect() which is called by execute()
+        datastore.connect() >> {
+            capturedTenantId = CurrentTenantHolder.get(datastore)
+            return Stub(Session) {
+                getDatastore() >> datastore
+            }
+        }
+
+        Tenants.withId(datastore, "initial") {
+             staticApi.withTenant("tenant1").exists(1L)
+        }
+
+        then: "The tenant context was correctly set during the call"
+        capturedTenantId == "tenant1"
+        
+        cleanup:
+        registry.metaClass = null
+        TestEntity.metaClass = null
+    }
+
+    void "execute with connection-name qualifier on DISCRIMINATOR multi-tenant entity does not override tenant context"() {
+        given: "a DISCRIMINATOR-mode child datastore that resolves its own connection qualifier"
+        def session = Stub(Session)
+        // childDatastore simulates a ChildHibernateDatastore: getDatastoreForConnection('secondary') returns itself
+        def childDatastore = Stub(MixedDatastore)
+        childDatastore.getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
+        childDatastore.hasCurrentSession() >> false
+        childDatastore.connect() >> session
+        childDatastore.getDatastoreForConnection("secondary") >> childDatastore
+        childDatastore.getConnectionSources() >> Stub(ConnectionSources) {
+            getDefaultConnectionSource() >> Stub(ConnectionSource) {
+                getName() >> "secondary"
+            }
+        }
+        session.getDatastore() >> childDatastore
+
+        def registry = GormRegistry.instance
+        registry.registerDatastore("secondary", childDatastore)
+
+        // The secondary static API has qualifier="secondary" and datastore=childDatastore,
+        // matching what GormRegistry.findStaticApi(entity, "secondary") returns in practice.
+        def secondaryApi = new DummyStaticApiForTest(TestEntity, childDatastore, [:], "secondary")
+        registry.registerEntityApis(TestEntity, secondaryApi, null, null)
+
+        when: "the secondary API executes inside an outer tenant context ('tenant1')"
+        def capturedTenantId = null
+        Tenants.withId(childDatastore, "tenant1") {
+            secondaryApi.withDatastoreSession { Session sess ->
+                capturedTenantId = CurrentTenantHolder.get(childDatastore)
+            }
+        }
+
+        then: "the connection qualifier does NOT override the enclosing tenant context"
+        capturedTenantId == "tenant1"
+
+        cleanup:
+        TestEntity.metaClass = null
+    }
+
+    void "execute with tenant-ID qualifier on DISCRIMINATOR multi-tenant entity binds that qualifier as tenant"() {
+        given: "a DISCRIMINATOR-mode parent datastore where 'tenant1' is not a known connection name"
+        def session = Stub(Session)
+        def datastore = Stub(MixedDatastore) {
+            getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
+            hasCurrentSession() >> false
+            connect() >> session
+            getConnectionSources() >> Stub(ConnectionSources) {
+                getDefaultConnectionSource() >> Stub(ConnectionSource) {
+                    getName() >> "default"
+                }
+            }
+            // 'tenant1' is not a datasource connection — getDatastoreForConnection returns null
+            getDatastoreForConnection("tenant1") >> null
+        }
+        session.getDatastore() >> datastore
+
+        def registry = GormRegistry.instance
+        registry.registerDatastore("default", datastore)
+
+        // withTenant("tenant1") produces an API with qualifier="tenant1"
+        def tenantApi = new DummyStaticApiForTest(TestEntity, datastore, [:], "tenant1")
+        registry.registerEntityApis(TestEntity, tenantApi, null, null)
+
+        when: "the tenant-qualified API executes a session callback"
+        def capturedTenantId = null
+        tenantApi.withDatastoreSession { Session sess ->
+            capturedTenantId = CurrentTenantHolder.get(datastore)
+        }
+
+        then: "the tenant ID qualifier is correctly bound as the current tenant"
+        capturedTenantId == "tenant1"
+
+        cleanup:
+        TestEntity.metaClass = null
+    }
+
+    void "findTransactionManager with qualifier returns transaction manager"() {
+        given:
+        def txManager = Stub(PlatformTransactionManager)
+        def datastore = Stub(TransactionCapableDatastore) {
+            getTransactionManager() >> txManager
+        }
+        def registry = GormRegistry.instance
+
+        when:
+        registry.registerDatastore("ds1", datastore)
+        registry.registerEntityDatastore(TestEntity.name, "ds1", datastore)
+
+        then:
+        registry.findTransactionManager(TestEntity, "ds1") == txManager
+    }
+
+    void "registerEntityApis and resolve APIs works as expected"() {
+        given:
+        def registry = GormRegistry.instance
+        def datastore = Stub(Datastore)
+        def validationApi = new GormValidationApi(TestEntity, datastore, registry)
+        def staticApi = new GormStaticApi(TestEntity, null, [], new DatastoreResolver() {
+            @Override Datastore resolve() { return datastore }
+        }, ConnectionSource.DEFAULT, registry)
+        def instanceApi = new GormInstanceApi(TestEntity, datastore, registry)
+
+        when:
+        registry.registerEntityApis(TestEntity, staticApi, instanceApi, validationApi)
+
+        then:
+        registry.resolveValidationApi(TestEntity) == validationApi
+        registry.resolveStaticApi(TestEntity) == staticApi
+        registry.resolveInstanceApi(TestEntity) == instanceApi
+    }
+
+    void "registerDatastoreByType registers datastore in discovery"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.registerDatastoreByType(datastore)
+
+        then:
+        registry.allDatastores.contains(datastore)
+        registry.datastoresByType.get(datastore.getClass()) == datastore
+    }
+
+    void "removeDatastoreByType(Datastore) removes from type registry"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.removeDatastoreByType(datastore)
+
+        then:
+        registry.allDatastores.contains(datastore)
+        !registry.datastoresByType.containsKey(datastore.getClass())
+    }
+
+    void "getDatastore with entity Class returns registered datastore"() {
+        given:
+        def datastore = Stub(Datastore)
+        def registry = GormRegistry.instance
+
+        when:
+        registry.initializeDatastore(datastore)
+        registry.registerEntityDatastore(TestEntity.name, ConnectionSource.DEFAULT, datastore)
+
+        then:
+        registry.getDatastore(TestEntity) == datastore
+    }
+
+    void "createDynamicFinders delegates to datastore api factory"() {
+        given:
+        def registry = GormRegistry.instance
+        def mappingContext = Stub(org.grails.datastore.mapping.model.MappingContext)
+        def datastore = Stub(Datastore) {
+            getMappingContext() >> mappingContext
+        }
+        def resolver = new DatastoreResolver() {
+            @Override Datastore resolve() { datastore }
+        }
+
+        when:
+        def finders = registry.createDynamicFinders(resolver, mappingContext)
+
+        then:
+        !finders.isEmpty()
+    }
+
+    void "registerEntity throws IllegalArgumentException for null arguments"() {
+        given:
+        def registry = GormRegistry.instance
+        def entity = Stub(PersistentEntity)
+
+        when:
+        registry.registerEntity(null, null)
+
+        then:
+        thrown(IllegalArgumentException)
+
+        when:
+        registry.registerEntity(entity, null)
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    interface MixedDatastore extends MultiTenantCapableDatastore, MultipleConnectionSourceCapableDatastore, Datastore {}
+    interface Datastore1 extends Datastore {}
+    interface Datastore2 extends Datastore {}
+
+    static class DummyStaticApiForTest extends GormStaticApi<TestEntity> {
+        Map sharedState
+        private final Datastore ds
+
+        DummyStaticApiForTest(Class<TestEntity> persistentClass, Datastore datastore, Map sharedState, String qualifier = "default") {
+            super(persistentClass, null, [], new DatastoreResolver() {
+                @Override Datastore resolve() { return datastore }
+            }, qualifier)
+            this.ds = datastore
+            this.sharedState = sharedState
+        }
+
+        @Override
+        Datastore getDatastore() { ds }
+
+        @Override
+        GormStaticApi<TestEntity> forQualifier(String qualifier) {
+            return new DummyStaticApiForTest(persistentClass, ds, sharedState, qualifier)
+        }
+    }
+
+    static class TestEntity implements MultiTenant<TestEntity> {
+        Long id
+    }
+}


(grails-core) 05/07: fix: mode-aware tenant lookup and bound DatastoreResolvers in GormRegistry

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 3067eb076d4ea69a8b3a4b4e899ef2646c3269fd
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jun 27 09:42:04 2026 -0500

    fix: mode-aware tenant lookup and bound DatastoreResolvers in GormRegistry
    
    findStaticApi/InstanceApi/ValidationApi: the tenant-lookup at priority-2 only
    checked CurrentTenantHolder.  In DATABASE and SCHEMA modes the tenant ID is never
    stored there explicitly — it comes from the TenantResolver (e.g. a subdomain or
    system-property resolver).  Consult the resolver for those strict modes so that
    per-tenant child APIs are selected correctly even when no tenant has been bound
    via Tenants.withId().  Guard with TenantNotFoundException propagation so missing
    tenants surface as errors rather than silently falling back to the default API.
    Also skip the API redirect when tenantId equals 'default' to avoid self-loops.
    
    createStaticApi / createInstanceApi / createValidationApi: replace the caller-
    supplied DatastoreResolver with a bound lambda that always returns the specific
    Datastore captured at registration time.  The old resolver was evaluated lazily
    at call time and could invoke tenant-resolution logic before any tenant context
    was active, causing spurious TenantNotFoundException during bootstrapping.
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../org/grails/datastore/gorm/GormRegistry.groovy  | 74 +++++++++++++++++-----
 1 file changed, 59 insertions(+), 15 deletions(-)

diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy
index 14da3f82c4..d833db021d 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy
@@ -35,6 +35,8 @@ import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCap
 import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
 import org.grails.datastore.mapping.reflect.NameUtils
 import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
@@ -461,13 +463,24 @@ class GormRegistry {
             // Priority 2: Check current bound tenant if using default qualifier
             Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
             if (ds instanceof MultiTenantCapableDatastore) {
-                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
-                if (tenantId != null) {
+                MultiTenantCapableDatastore mtds = (MultiTenantCapableDatastore) ds
+                MultiTenancySettings.MultiTenancyMode mode = mtds.getMultiTenancyMode()
+                boolean strictMode = mode == MultiTenancySettings.MultiTenancyMode.DATABASE ||
+                        mode == MultiTenancySettings.MultiTenancyMode.SCHEMA
+                Serializable tenantId = CurrentTenantHolder.get(mtds)
+                if (tenantId == null && strictMode) {
+                    try {
+                        tenantId = mtds.tenantResolver.resolveTenantIdentifier()
+                    } catch (TenantNotFoundException e) {
+                        throw e
+                    }
+                }
+                if (tenantId != null && !ConnectionSource.DEFAULT.equals(tenantId.toString())) {
                     GormStaticApi api = staticApiRegistry.getDirect(normalizedClassName, tenantId.toString())
                     if (api != null) return api
                 }
             }
-            
+
             // Priority 3: Fall back to default API instance if specialized one not found,
             // but keep the qualifier so the API can handle tenant binding
             if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
@@ -495,13 +508,24 @@ class GormRegistry {
 
             Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
             if (ds instanceof MultiTenantCapableDatastore) {
-                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
-                if (tenantId != null) {
+                MultiTenantCapableDatastore mtds = (MultiTenantCapableDatastore) ds
+                MultiTenancySettings.MultiTenancyMode mode = mtds.getMultiTenancyMode()
+                boolean strictMode = mode == MultiTenancySettings.MultiTenancyMode.DATABASE ||
+                        mode == MultiTenancySettings.MultiTenancyMode.SCHEMA
+                Serializable tenantId = CurrentTenantHolder.get(mtds)
+                if (tenantId == null && strictMode) {
+                    try {
+                        tenantId = mtds.tenantResolver.resolveTenantIdentifier()
+                    } catch (TenantNotFoundException e) {
+                        throw e
+                    }
+                }
+                if (tenantId != null && !ConnectionSource.DEFAULT.equals(tenantId.toString())) {
                     GormInstanceApi api = instanceApiRegistry.getDirect(normalizedClassName, tenantId.toString())
                     if (api != null) return api
                 }
             }
-            
+
             if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
                 GormInstanceApi api = instanceApiRegistry.getDirect(normalizedClassName, ConnectionSource.DEFAULT)
                 if (api != null) return api
@@ -535,13 +559,24 @@ class GormRegistry {
 
             Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
             if (ds instanceof MultiTenantCapableDatastore) {
-                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
-                if (tenantId != null) {
+                MultiTenantCapableDatastore mtds = (MultiTenantCapableDatastore) ds
+                MultiTenancySettings.MultiTenancyMode mode = mtds.getMultiTenancyMode()
+                boolean strictMode = mode == MultiTenancySettings.MultiTenancyMode.DATABASE ||
+                        mode == MultiTenancySettings.MultiTenancyMode.SCHEMA
+                Serializable tenantId = CurrentTenantHolder.get(mtds)
+                if (tenantId == null && strictMode) {
+                    try {
+                        tenantId = mtds.tenantResolver.resolveTenantIdentifier()
+                    } catch (TenantNotFoundException e) {
+                        throw e
+                    }
+                }
+                if (tenantId != null && !ConnectionSource.DEFAULT.equals(tenantId.toString())) {
                     GormValidationApi api = validationApiRegistry.getDirect(normalizedClassName, tenantId.toString())
                     if (api != null) return api
                 }
             }
-            
+
             if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
                 GormValidationApi api = validationApiRegistry.getDirect(normalizedClassName, ConnectionSource.DEFAULT)
                 if (api != null) return api
@@ -787,24 +822,33 @@ class GormRegistry {
     }
 
     /**
-     * Create a GormStaticApi instance
+     * Create a GormStaticApi instance.
+     * Uses a bound resolver (always returns the given datastore) at registration time so that
+     * tenant-resolving DatastoreResolvers are not invoked before any tenant context is active.
      */
     GormStaticApi createStaticApi(Class cls, Datastore datastore, DatastoreResolver resolver, String qualifier) {
-        return getApiFactory(datastore).createStaticApi(cls, datastore.mappingContext, resolver, qualifier, this)
+        DatastoreResolver boundResolver = { datastore } as DatastoreResolver
+        return getApiFactory(datastore).createStaticApi(cls, datastore.mappingContext, boundResolver, qualifier, this)
     }
 
     /**
-     * Create a GormInstanceApi instance
+     * Create a GormInstanceApi instance.
+     * Uses a bound resolver (always returns the given datastore) at registration time so that
+     * tenant-resolving DatastoreResolvers are not invoked before any tenant context is active.
      */
     GormInstanceApi createInstanceApi(Class cls, Datastore datastore, DatastoreResolver resolver, boolean failOnError, boolean markDirty) {
-        return getApiFactory(datastore).createInstanceApi(cls, datastore.mappingContext, resolver, this, failOnError, markDirty)
+        DatastoreResolver boundResolver = { datastore } as DatastoreResolver
+        return getApiFactory(datastore).createInstanceApi(cls, datastore.mappingContext, boundResolver, this, failOnError, markDirty)
     }
 
     /**
-     * Create a GormValidationApi instance
+     * Create a GormValidationApi instance.
+     * Uses a bound resolver (always returns the given datastore) at registration time so that
+     * tenant-resolving DatastoreResolvers are not invoked before any tenant context is active.
      */
     GormValidationApi createValidationApi(Class cls, Datastore datastore, DatastoreResolver resolver) {
-        return getApiFactory(datastore).createValidationApi(cls, datastore.mappingContext, resolver, this)
+        DatastoreResolver boundResolver = { datastore } as DatastoreResolver
+        return getApiFactory(datastore).createValidationApi(cls, datastore.mappingContext, boundResolver, this)
     }
 
     /**


(grails-core) 01/07: feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 3901900ab1799ad4f8b6d658f9a7aed302aee37f
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jun 27 11:21:32 2026 -0500

    feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API refactor
    
    Introduce GormRegistry singleton replacing O(M×N) static maps in GormEnhancer.
    APIs are registered once at entity-registration time and looked up in O(1).
    
    - GormRegistry: singleton keyed by (entityClass, qualifier); handles MultiTenant
      qualifier expansion, thread-local preferred datastore, and concurrent-safe removal
    - GormApiFactory / DefaultGormApiFactory: pluggable factory per datastore type
    - GormApiResolver: routes static/instance/validation API lookups through the registry
    - GormEnhancer: delegates all registration and lookup to GormRegistry
    - GormStaticApi / GormInstanceApi / GormValidationApi: use DatastoreResolver instead
      of holding a direct Datastore reference; support qualifier-aware execution
    - AbstractGormApi.execute(): distinguishes datasource connection qualifiers from
      tenant-ID qualifiers to avoid overwriting the active tenant context
    - CurrentTenantHolder: thread-safe tenant binding for DISCRIMINATOR multi-tenancy
    - ServiceTransformation / TransactionalTransform: resolve transaction manager via
      GormRegistry instead of static map lookups
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .gitignore                                         |    5 +
 build.gradle                                       |   12 +
 gradle/hibernate5-test-config.gradle               |    1 +
 gradle/hibernate7-test-config.gradle               |    1 +
 gradle/mongodb-test-config.gradle                  |    1 +
 gradle/rat-root-config.gradle                      |    1 +
 gradle/test-config.gradle                          |   38 +-
 grails-datamapping-core/build.gradle               |    1 +
 .../main/groovy/grails/gorm/CriteriaBuilder.java   |   21 +
 .../groovy/grails/gorm/DetachedCriteria.groovy     |   10 +-
 .../src/main/groovy/grails/gorm/MultiTenant.groovy |    8 +-
 .../grails/gorm/api/GormStaticOperations.groovy    |   27 +
 .../gorm/multitenancy/CurrentTenantHolder.groovy   |  135 ++
 .../groovy/grails/gorm/multitenancy/Tenants.groovy |  214 ++--
 .../transactions/GrailsTransactionTemplate.groovy  |   22 +-
 .../datastore/gorm/AbstractDatastoreApi.groovy     |   29 +-
 .../grails/datastore/gorm/AbstractGormApi.groovy   |  171 ++-
 .../datastore/gorm/AbstractGormApiRegistry.groovy  |  152 +++
 .../gorm/ConnectionSourceNameResolver.groovy       |   70 +
 .../FindByFinder.java => DatastoreResolver.groovy} |   29 +-
 .../datastore/gorm/DefaultGormApiFactory.groovy    |   83 ++
 .../grails/datastore/gorm/GormApiFactory.groovy    |   52 +
 .../grails/datastore/gorm/GormApiResolver.groovy   |  414 ++++++
 .../org/grails/datastore/gorm/GormEnhancer.groovy  |  729 +++--------
 .../datastore/gorm/GormEnhancerRegistry.groovy     |   99 ++
 .../org/grails/datastore/gorm/GormEntity.groovy    |   53 +-
 .../datastore/gorm/GormEntityDirtyCheckable.groovy |    8 +-
 .../grails/datastore/gorm/GormInstanceApi.groovy   |  438 +++----
 .../datastore/gorm/GormInstanceApiRegistry.groovy  |   57 +
 .../org/grails/datastore/gorm/GormRegistry.groovy  |  898 +++++++++++++
 .../org/grails/datastore/gorm/GormStaticApi.groovy | 1347 ++++++++------------
 .../datastore/gorm/GormStaticApiRegistry.groovy    |   57 +
 .../grails/datastore/gorm/GormValidateable.groovy  |    6 +-
 .../grails/datastore/gorm/GormValidationApi.groovy |  135 +-
 .../gorm/GormValidationApiRegistry.groovy          |   57 +
 .../gorm/events/AutoTimestampEventListener.java    |    7 +-
 .../gorm/finders/AbstractFindByFinder.java         |    8 +-
 .../datastore/gorm/finders/AbstractFinder.java     |   29 +-
 .../datastore/gorm/finders/CountByFinder.java      |   55 +-
 .../datastore/gorm/finders/DynamicFinder.java      |   57 +-
 .../gorm/finders/FindAllByBooleanFinder.java       |   18 +-
 .../datastore/gorm/finders/FindAllByFinder.java    |   13 +-
 .../gorm/finders/FindByBooleanFinder.java          |   25 +-
 .../datastore/gorm/finders/FindByFinder.java       |    5 +
 .../gorm/finders/FindOrCreateByFinder.java         |    9 +
 .../datastore/gorm/finders/FindOrSaveByFinder.java |   48 +-
 .../datastore/gorm/finders/ListOrderByFinder.java  |   77 +-
 .../gorm/jdbc/MultiTenantConnection.groovy         |    8 +-
 .../gorm/jdbc/schema/DefaultSchemaHandler.groovy   |    3 +
 .../multitenancy/MultiTenantEventListener.java     |   68 +-
 .../TenantDelegatingGormOperations.groovy          |   28 +
 .../multitenancy/transform/TenantTransform.groovy  |   27 +-
 .../query/criteria/AbstractCriteriaBuilder.java    |   29 +-
 .../gorm/services/DefaultTenantService.groovy      |   66 +-
 .../gorm/services/DefaultTransactionService.groovy |   13 +
 .../implementers/AbstractServiceImplementer.groovy |   49 +-
 .../AbstractStringQueryImplementer.groovy          |   59 +-
 .../implementers/FindAllByImplementer.groovy       |   10 +-
 ...nterfaceProjectionStringQueryImplementer.groovy |    5 +
 .../FindOneStringQueryImplementer.groovy           |   21 +-
 .../UpdateStringQueryImplementer.groovy            |    5 +
 .../transform/ServiceTransformation.groovy         |  280 ++--
 .../DefaultTransactionTemplateFactory.groovy       |   51 +
 .../transactions/TransactionTemplateFactory.groovy |   61 +
 .../transform/TransactionalTransform.groovy        |  426 +++----
 ...tDatastoreMethodDecoratingTransformation.groovy |  202 ++-
 .../AbstractMethodDecoratingTransformation.groovy  |    7 +-
 .../constraints/builtin/UniqueConstraint.groovy    |    4 +-
 .../validation/jakarta/GormValidatorAdapter.groovy |   17 +-
 .../MappingContextTraversableResolver.groovy       |    3 +
 .../listener/ValidationEventListener.groovy        |    4 +-
 71 files changed, 4663 insertions(+), 2515 deletions(-)

diff --git a/.gitignore b/.gitignore
index be3f3c371d..033c7a6e53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,8 @@ etc/bin/results
 node_modules/
 local-tasks.gradle
 local-init.gradle
+.junie/
+.antigravitycli/
+.clinerules
+.cursorrules
+.windsurfrules
diff --git a/build.gradle b/build.gradle
index 87b54c25f2..d1985458bd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -84,6 +84,18 @@ subprojects {
             cacheChangingModulesFor(cacheHours, 'hours')
         }
     }
+
+    tasks.withType(Test).configureEach {
+        systemProperty 'logging.level.org.testcontainers', 'WARN'
+        systemProperty 'logging.level.com.github.dockerjava', 'WARN'
+        systemProperty 'logging.level.tc', 'WARN'
+        systemProperty 'logging.level.asset.pipeline', 'WARN'
+        systemProperty 'logging.level.org.springframework', 'WARN'
+        systemProperty 'logging.level.org.hibernate', 'WARN'
+        systemProperty 'logging.level.com.zaxxer.hikari', 'WARN'
+        systemProperty 'logging.level.grails.config.external', 'WARN'
+        systemProperty 'logging.level.org.apache.grails', 'WARN'
+    }
 }
 
 interface ExecSupport {
diff --git a/gradle/hibernate5-test-config.gradle b/gradle/hibernate5-test-config.gradle
index afaa18f8f7..ae47fb5fcc 100644
--- a/gradle/hibernate5-test-config.gradle
+++ b/gradle/hibernate5-test-config.gradle
@@ -28,6 +28,7 @@ tasks.withType(Test).configureEach {
     outputs.cacheIf { !doNotCacheTests }
     outputs.upToDateWhen { !doNotCacheTests }
 
+
     onlyIf {
         ![
                 'onlyFunctionalTests',
diff --git a/gradle/hibernate7-test-config.gradle b/gradle/hibernate7-test-config.gradle
index 5d03dff9f1..4528a4e46c 100644
--- a/gradle/hibernate7-test-config.gradle
+++ b/gradle/hibernate7-test-config.gradle
@@ -28,6 +28,7 @@ tasks.withType(Test).configureEach {
     outputs.cacheIf { !doNotCacheTests }
     outputs.upToDateWhen { !doNotCacheTests }
 
+
     onlyIf {
         ![
                 'onlyFunctionalTests',
diff --git a/gradle/mongodb-test-config.gradle b/gradle/mongodb-test-config.gradle
index d7a056b2d6..9ad64b026c 100644
--- a/gradle/mongodb-test-config.gradle
+++ b/gradle/mongodb-test-config.gradle
@@ -47,6 +47,7 @@ tasks.withType(Test).configureEach {
 
     useJUnitPlatform()
     maxParallelForks = 1
+
     jvmArgs = ['-Xmx1028M']
     afterSuite {
         System.out.print('.')
diff --git a/gradle/rat-root-config.gradle b/gradle/rat-root-config.gradle
index acd52b23db..0e17ffa401 100644
--- a/gradle/rat-root-config.gradle
+++ b/gradle/rat-root-config.gradle
@@ -133,6 +133,7 @@ tasks.named('rat') {
             'buildSrc/build/**', // build directories
             '**/*.log', // exclude log files
             'local-tasks.gradle', // exclude local helper scripts
+            '**/ISSUES.md', // exclude local issue tracking files (no license header)
     ] + rootProject.subprojects.collect{"${rootProject.projectDir.relativePath(it.layout.buildDirectory.get().asFile).toString()}/**/*" }
     // logger.lifecycle("Excludes for RAT task: ${allExcludes.join(', \n')}")
     excludes = allExcludes
diff --git a/gradle/test-config.gradle b/gradle/test-config.gradle
index 604bc9304f..89ce5facfd 100644
--- a/gradle/test-config.gradle
+++ b/gradle/test-config.gradle
@@ -31,6 +31,38 @@ dependencies {
     add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher')
 }
 
+/**
+ * Recursively checks if a project has a direct or transitive dependency on a target project.
+ * Used to detect modules that rely on GORM (grails-datamapping-core), which are prone to
+ * cross-test registry pollution when executed in parallel (maxParallelForks > 1).
+ *
+ * Uses Gradle 9 compatible path lookup (proj.project(pd.getPath())) because
+ * ProjectDependency.getDependencyProject() was removed in Gradle 9.
+ *
+ * @param proj the project to inspect
+ * @param targetProjectName the name of the target dependency project (e.g. 'grails-datamapping-core')
+ * @param visited set of already visited projects to prevent infinite recursion in cyclic dependency configurations
+ * @return true if the project depends on the target project
+ */
+@groovy.transform.CompileStatic
+boolean dependsOnProject(Project proj, String targetProjectName, Set<String> visited = new HashSet<String>()) {
+    if (proj.name == targetProjectName) return true
+    if (visited.contains(proj.name)) return false
+    visited.add(proj.name)
+    for (org.gradle.api.artifacts.Configuration config : proj.configurations) {
+        for (org.gradle.api.artifacts.Dependency dep : config.dependencies) {
+            if (dep instanceof org.gradle.api.artifacts.ProjectDependency) {
+                org.gradle.api.artifacts.ProjectDependency pd = (org.gradle.api.artifacts.ProjectDependency) dep
+                Project depProj = proj.project(pd.getPath())
+                if (dependsOnProject(depProj, targetProjectName, visited)) {
+                    return true
+                }
+            }
+        }
+    }
+    return false
+}
+
 // Disable build cache for Groovy compilation in CI to ensure AST transformations are always applied.
 // AST transformers are applied at compile time, and Gradle's incremental compilation might not detect
 // when a transformer itself changes, leading to stale bytecode.
@@ -79,7 +111,11 @@ tasks.withType(Test).configureEach {
         showStackTraces = true
     }
     excludes = ['**/*TestCase.class', '**/*$*.class']
-    maxParallelForks = configuredTestParallel
+    
+    // Selectively isolate GORM (grails-datamapping-core) dependent tests to prevent GormRegistry conflicts
+    def isGormProject = dependsOnProject(project, 'grails-datamapping-core')
+    maxParallelForks = isGormProject ? 1 : configuredTestParallel
+    
     maxHeapSize = isCiBuild ? '768m' : '1024m'
     forkEvery = hasProperty('forkEveryUnitTest') ? getProperty('forkEveryUnitTest') as long : (isCiBuild ? 50 : 100)
     if (System.getProperty('debug.tests')) {
diff --git a/grails-datamapping-core/build.gradle b/grails-datamapping-core/build.gradle
index 6027dc56b9..46969488e0 100644
--- a/grails-datamapping-core/build.gradle
+++ b/grails-datamapping-core/build.gradle
@@ -106,6 +106,7 @@ dependencies {
     testImplementation project(':grails-core'), {
         // impl: ValidationException
     }
+    testImplementation project(':grails-data-simple')
     testImplementation 'org.junit.jupiter:junit-jupiter-api'
     testImplementation 'org.spockframework:spock-core'
 
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java b/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
index f5458518a7..7e07a099cb 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java
@@ -173,4 +173,25 @@ public class CriteriaBuilder<T> extends AbstractCriteriaBuilder implements Build
     public Object scroll(@DelegatesTo(Criteria.class) Closure c) {
         return invokeMethod(SCROLL_CALL, new Object[]{c});
     }
+
+    /**
+     * Executes the criteria builder
+     *
+     * @param c The closure
+     * @return The result
+     */
+    public Object call(@DelegatesTo(Criteria.class) Closure c) {
+        ensureQueryIsInitialized();
+        uniqueResult = false;
+        invokeClosureNode(c);
+        
+        Object result;
+        if (!uniqueResult) {
+            result = invokeList();
+        }
+        else {
+            result = query.singleResult();
+        }
+        return result;
+    }
 }
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/DetachedCriteria.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/DetachedCriteria.groovy
index 2ee44a19eb..547742116c 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/DetachedCriteria.groovy
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/DetachedCriteria.groovy
@@ -24,7 +24,7 @@ import groovy.util.logging.Slf4j
 
 import jakarta.persistence.criteria.JoinType
 
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormStaticApi
 import org.grails.datastore.gorm.finders.DynamicFinder
 import org.grails.datastore.gorm.query.GormOperations
@@ -556,7 +556,7 @@ class DetachedCriteria<T> extends AbstractDetachedCriteria<T> implements GormOpe
      * @return The total number deleted
      */
     Number deleteAll() {
-        GormEnhancer.findStaticApi(targetClass, connectionName).withDatastoreSession { Session session ->
+        GormRegistry.instance.findStaticApi(targetClass, connectionName).withDatastoreSession { Session session ->
             applyLazyCriteria()
             session.deleteAll(this)
         }
@@ -568,7 +568,7 @@ class DetachedCriteria<T> extends AbstractDetachedCriteria<T> implements GormOpe
      * @return The total number updated
      */
     Number updateAll(Map properties) {
-        GormEnhancer.findStaticApi(targetClass, connectionName).withDatastoreSession { Session session ->
+        GormRegistry.instance.findStaticApi(targetClass, connectionName).withDatastoreSession { Session session ->
             applyLazyCriteria()
             session.updateAll(this, properties)
         }
@@ -737,7 +737,7 @@ class DetachedCriteria<T> extends AbstractDetachedCriteria<T> implements GormOpe
 
     private withPopulatedQuery(Map args, Closure additionalCriteria, Closure callable)  {
 
-        GormStaticApi staticApi = persistentEntity.isMultiTenant() ? GormEnhancer.findStaticApi(targetClass) : GormEnhancer.findStaticApi(targetClass, connectionName)
+        GormStaticApi staticApi = GormRegistry.instance.findStaticApi(targetClass, connectionName)
         staticApi.withDatastoreSession { Session session ->
             applyLazyCriteria()
             Query query
@@ -767,7 +767,7 @@ class DetachedCriteria<T> extends AbstractDetachedCriteria<T> implements GormOpe
 
             DynamicFinder.populateArgumentsForCriteria(targetClass, query, args)
 
-            callable.call(query)
+            return callable.call(query)
         }
     }
 
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/MultiTenant.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/MultiTenant.groovy
index 7f429caca1..b85d45c8c8 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/MultiTenant.groovy
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/MultiTenant.groovy
@@ -23,7 +23,7 @@ import groovy.transform.CompileStatic
 import groovy.transform.Generated
 
 import grails.gorm.api.GormAllOperations
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.mapping.core.connections.ConnectionSource
 
 /**
@@ -44,7 +44,7 @@ trait MultiTenant<D> extends Entity {
      */
     @Generated
     static <T> T withTenant(Serializable tenantId, Closure<T> callable) {
-        GormEnhancer.findStaticApi(this).withTenant(tenantId, callable)
+        GormRegistry.instance.findStaticApi((Class<D>) this).withTenant(tenantId, callable)
     }
 
     /**
@@ -55,7 +55,7 @@ trait MultiTenant<D> extends Entity {
      */
     @Generated
     static <D> GormAllOperations eachTenant(Closure callable) {
-        GormEnhancer.findStaticApi(this, ConnectionSource.DEFAULT).eachTenant(callable)
+        GormRegistry.instance.findStaticApi((Class<D>) this, ConnectionSource.DEFAULT).eachTenant(callable)
     }
 
     /**
@@ -66,6 +66,6 @@ trait MultiTenant<D> extends Entity {
      */
     @Generated
     static <D> GormAllOperations<D> withTenant(Serializable tenantId) {
-        (GormAllOperations<D>) GormEnhancer.findStaticApi(this).withTenant(tenantId)
+        (GormAllOperations<D>) GormRegistry.instance.findStaticApi((Class<D>) this).withTenant(tenantId)
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/api/GormStaticOperations.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/api/GormStaticOperations.groovy
index 667488b481..ada4b10fa7 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/api/GormStaticOperations.groovy
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/api/GormStaticOperations.groovy
@@ -105,18 +105,45 @@ interface GormStaticOperations<D> {
      */
     List<Serializable> saveAll(Iterable<?> objectsToSave)
 
+    /**
+     * Deletes all objects
+     * @return The number of objects deleted
+     */
+    Number deleteAll()
+
+    /**
+     * Deletes all objects for the given arguments
+     * @param params The arguments
+     * @return The number of objects deleted
+     */
+    Number deleteAll(Map params)
+
     /**
      * Deletes a list of objects in one go
      * @param objectsToDelete The objects to delete
      */
     void deleteAll(Object... objectsToDelete)
 
+    /**
+     * Deletes a list of objects in one go
+     * @param params The arguments
+     * @param objectsToDelete The objects to delete
+     */
+    void deleteAll(Map params, Object... objectsToDelete)
+
     /**
      * Deletes a list of objects in one go
      * @param objectsToDelete Collection of objects to delete
      */
     void deleteAll(Iterable objectToDelete)
 
+    /**
+     * Deletes a list of objects in one go
+     * @param params The arguments
+     * @param objectsToDelete Collection of objects to delete
+     */
+    void deleteAll(Map params, Iterable objectsToDelete)
+
     /**
      * Creates an instance of this class
      * @return The created instance
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/CurrentTenantHolder.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/CurrentTenantHolder.groovy
new file mode 100644
index 0000000000..a3b9e27c88
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/CurrentTenantHolder.groovy
@@ -0,0 +1,135 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.gorm.multitenancy
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+
+@CompileStatic
+class CurrentTenantHolder {
+
+    private static final ThreadLocal<Map<Object, Serializable>> currentTenantThreadLocal = new ThreadLocal<Map<Object, Serializable>>() {
+        @Override
+        protected Map<Object, Serializable> initialValue() {
+            return new HashMap<>()
+        }
+    }
+
+    /**
+     * @return Obtain the current tenant (fallback for any datastore)
+     */
+    static Serializable get() {
+        def map = currentTenantThreadLocal.get()
+        if (!map.isEmpty()) {
+            return map.values().iterator().next()
+        }
+        return null
+    }
+
+    /**
+     * @return Obtain the current tenant
+     */
+    static Serializable get(Datastore datastore) {
+        def map = currentTenantThreadLocal.get()
+        def tenantId = map.get(datastore)
+        if (tenantId == null) {
+            tenantId = map.get(datastore.getClass())
+        }
+        return tenantId
+    }
+
+    /**
+     * Set the current tenant
+     *
+     * @param tenantId The tenant id
+     */
+    static void set(Datastore datastore, Serializable tenantId) {
+        currentTenantThreadLocal.get().put(datastore, tenantId)
+    }
+
+    static void set(Class<? extends Datastore> datastoreClass, Serializable tenantId) {
+        currentTenantThreadLocal.get().put(datastoreClass, tenantId)
+    }
+
+    static void remove(Datastore datastore) {
+        currentTenantThreadLocal.get().remove(datastore)
+    }
+
+    static void remove(Class<? extends Datastore> datastoreClass) {
+        currentTenantThreadLocal.get().remove(datastoreClass)
+    }
+
+    /**
+     * Execute with the current tenant
+     *
+     * @param callable The closure
+     * @return The result of the closure
+     */
+    static <T> T withTenant(Datastore datastore, Serializable tenantId, Closure<T> callable) {
+        def previous = currentTenantThreadLocal.get().get(datastore)
+        try {
+            set(datastore, tenantId)
+            callable.call(tenantId)
+        } finally {
+            if (previous == null) {
+                remove(datastore)
+            }
+            else {
+                set(datastore, previous)
+            }
+        }
+    }
+
+    static <T> T withTenant(Class<? extends Datastore> datastoreClass, Serializable tenantId, Closure<T> callable) {
+        def previous = currentTenantThreadLocal.get().get(datastoreClass)
+        try {
+            set(datastoreClass, tenantId)
+            callable.call(tenantId)
+        } finally {
+            if (previous == null) {
+                remove(datastoreClass)
+            }
+            else {
+                set(datastoreClass, previous)
+            }
+        }
+    }
+
+    /**
+     * Execute without current tenant
+     *
+     * @param callable The closure
+     * @return The result of the closure
+     */
+    static <T> T withoutTenant(Datastore datastore, Closure<T> callable) {
+        def previous = currentTenantThreadLocal.get().get(datastore)
+        try {
+            set(datastore, (Serializable) ConnectionSource.DEFAULT)
+            callable.call()
+        } finally {
+            if (previous == null) {
+                remove(datastore)
+            } else {
+                set(datastore, previous)
+            }
+        }
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy
index abbfb17ce9..2017a3a9d2 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy
@@ -22,7 +22,7 @@ package grails.gorm.multitenancy
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSources
@@ -41,6 +41,37 @@ import org.grails.datastore.mapping.multitenancy.TenantResolver
 @Slf4j
 class Tenants {
 
+    /**
+     * Pluggable locator for Datastore instances, allowing for easier testing.
+     */
+    static DatastoreLocator datastoreLocator = new DatastoreLocator()
+
+    static class DatastoreLocator {
+        Datastore getDatastore() {
+            GormRegistry.instance.apiResolver.findSingleDatastore()
+        }
+        Datastore getDatastore(Class<? extends Datastore> datastoreClass) {
+            GormRegistry.instance.apiResolver.findDatastoreByType(datastoreClass)
+        }
+        Datastore getDatastoreForDomain(Class domainClass) {
+            GormRegistry.instance.apiResolver.findDatastore(domainClass)
+        }
+    }
+
+    /**
+     * Execute the given closure with the given tenant id.
+     *
+     * @param tenantId The tenant id
+     * @param callable The closure
+     * @return The result of the closure
+     */
+    static <T> T withTenant(Serializable tenantId, Closure<T> callable) {
+        Datastore datastore = datastoreLocator.getDatastore()
+        return CurrentTenantHolder.withTenant(datastore.getClass(), tenantId) {
+            return CurrentTenantHolder.withTenant(datastore, tenantId, callable)
+        }
+    }
+
     /**
      * Execute the given closure for each tenant.
      *
@@ -48,7 +79,7 @@ class Tenants {
      * @return The result of the closure
      */
     static void eachTenant(Closure callable) {
-        Datastore datastore = GormEnhancer.findSingleDatastore()
+        Datastore datastore = datastoreLocator.getDatastore()
         eachTenantInternal(datastore, callable)
     }
 
@@ -59,7 +90,7 @@ class Tenants {
      * @return The result of the closure
      */
     static void eachTenant(Class<? extends Datastore> datastoreClass, Closure callable) {
-        eachTenantInternal(GormEnhancer.findDatastoreByType(datastoreClass), callable)
+        eachTenantInternal(datastoreLocator.getDatastore(datastoreClass), callable)
     }
 
     /**
@@ -68,7 +99,7 @@ class Tenants {
      * @throws org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException if no current tenant is found
      */
     static Serializable currentId() {
-        Datastore datastore = GormEnhancer.findSingleDatastore()
+        Datastore datastore = datastoreLocator.getDatastore()
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
             return currentId(multiTenantCapableDatastore)
@@ -85,15 +116,13 @@ class Tenants {
      * @return The current id
      */
     static Serializable currentId(MultiTenantCapableDatastore multiTenantCapableDatastore) {
-        def tenantId = CurrentTenant.get()
+        def tenantId = CurrentTenantHolder.get(multiTenantCapableDatastore)
         if (tenantId != null) {
-            log.debug('Found tenant id [{}] bound to thread local', tenantId)
             return tenantId
         } else {
             TenantResolver tenantResolver = multiTenantCapableDatastore.getTenantResolver()
-            Serializable tenantIdentifier = tenantResolver.resolveTenantIdentifier()
-            log.debug('Resolved tenant id [{}] from resolver [{}]', tenantIdentifier, tenantResolver.getClass().simpleName)
-            return tenantIdentifier
+            def resolved = tenantResolver.resolveTenantIdentifier()
+            return resolved
         }
     }
 
@@ -103,20 +132,9 @@ class Tenants {
      * @throws org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException if no current tenant is found
      */
     static Serializable currentId(Class<? extends Datastore> datastoreClass) {
-        Datastore datastore = GormEnhancer.findDatastoreByType(datastoreClass)
+        Datastore datastore = datastoreLocator.getDatastore(datastoreClass)
         if (datastore instanceof MultiTenantCapableDatastore) {
-            MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
-            def tenantId = CurrentTenant.get()
-            if (tenantId != null) {
-                log.debug('Found tenant id [{}] bound to thread local', tenantId)
-                return tenantId
-            }
-            else {
-                TenantResolver tenantResolver = multiTenantCapableDatastore.getTenantResolver()
-                def tenantIdentifier = tenantResolver.resolveTenantIdentifier()
-                log.debug('Resolved tenant id [{}] from resolver [{}]', tenantIdentifier, tenantResolver.getClass().simpleName)
-                return tenantIdentifier
-            }
+            return currentId((MultiTenantCapableDatastore) datastore)
         }
         else {
             throw new UnsupportedOperationException('Datastore implementation does not support multi-tenancy')
@@ -131,7 +149,7 @@ class Tenants {
      * @return The result of the closure
      */
     static <T> T withoutId(Closure<T> callable) {
-        Datastore datastore = GormEnhancer.findSingleDatastore()
+        Datastore datastore = datastoreLocator.getDatastore()
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
             return withoutId(multiTenantCapableDatastore, callable)
@@ -147,10 +165,10 @@ class Tenants {
      * @return The result of the closure
      */
     static <T> T withCurrent(Closure<T> callable) {
-        Serializable tenantIdentifier = currentId()
-        Datastore datastore = GormEnhancer.findSingleDatastore()
+        Datastore datastore = datastoreLocator.getDatastore()
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
+            Serializable tenantIdentifier = currentId(multiTenantCapableDatastore)
             return withId(multiTenantCapableDatastore, tenantIdentifier, callable)
         }
         else {
@@ -166,10 +184,10 @@ class Tenants {
      * @return The result of the closure
      */
     static <T> T withCurrent(Class<? extends Datastore> datastoreClass, Closure<T> callable) {
-        Serializable tenantIdentifier = currentId(datastoreClass)
-        Datastore datastore = GormEnhancer.findDatastoreByType(datastoreClass)
+        Datastore datastore = datastoreLocator.getDatastore(datastoreClass)
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
+            Serializable tenantIdentifier = currentId(multiTenantCapableDatastore)
             return withId(multiTenantCapableDatastore, tenantIdentifier, callable)
         }
         else {
@@ -184,7 +202,7 @@ class Tenants {
      * @return The result of the closure
      */
     static <T> T withId(Serializable tenantId, Closure<T> callable) {
-        Datastore datastore = GormEnhancer.findSingleDatastore()
+        Datastore datastore = datastoreLocator.getDatastore()
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
             return withId(multiTenantCapableDatastore, tenantId, callable)
@@ -193,14 +211,15 @@ class Tenants {
             throw new UnsupportedOperationException('Datastore implementation does not support multi-tenancy')
         }
     }
+
     /**
      * Execute the given closure with given tenant id
      * @param tenantId The tenant id
      * @param callable The closure
      * @return The result of the closure
      */
-    static <T> T withId(Class<? extends Datastore> datastoreClass, Serializable tenantId, Closure<T> callable) {
-        Datastore datastore = GormEnhancer.findDatastoreByType(datastoreClass)
+    static <T> T withId(Class domainClass, Serializable tenantId, Closure<T> callable) {
+        Datastore datastore = datastoreLocator.getDatastoreForDomain(domainClass)
         if (datastore instanceof MultiTenantCapableDatastore) {
             MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
             return withId(multiTenantCapableDatastore, tenantId, callable)
@@ -210,13 +229,26 @@ class Tenants {
         }
     }
 
+    /**
+     * Execute the given closure with given tenant id for the given datastore. This method will create a new datastore session for the scope of the call and hence is designed to be used to manage the connection life cycle
+     * @param tenantId The tenant id
+     * @param callable The closure
+     * @return The result of the closure
+     */
+    static <T> T withTenant(Class domainClass, Serializable tenantId, Closure<T> callable) {
+        Datastore datastore = datastoreLocator.getDatastoreForDomain(domainClass)
+        return CurrentTenantHolder.withTenant(datastore.getClass(), tenantId) {
+            return CurrentTenantHolder.withTenant(datastore, tenantId, callable)
+        }
+    }
+
     /**
      * Execute the given closure without tenant id for the given datastore. This method will create a new datastore session for the scope of the call and hence is designed to be used to manage the connection life cycle
      * @param callable The closure
      * @return The result of the closure
      */
     static <T> T withoutId(MultiTenantCapableDatastore multiTenantCapableDatastore, Closure<T> callable) {
-        return CurrentTenant.withoutTenant {
+        return CurrentTenantHolder.withoutTenant(multiTenantCapableDatastore) {
             if (multiTenantCapableDatastore.getMultiTenancyMode().isSharedConnection()) {
                 def i = callable.parameterTypes.length
                 if (i == 0) {
@@ -254,22 +286,48 @@ class Tenants {
      * @return The result of the closure
      */
     static <T> T withId(MultiTenantCapableDatastore multiTenantCapableDatastore, Serializable tenantId, Closure<T> callable) {
-        return CurrentTenant.withTenant(tenantId) {
+        log.debug('Tenants.withId called for datastore {} with tenantId {}', multiTenantCapableDatastore, tenantId)
+        org.grails.datastore.mapping.core.Datastore childDatastore = null
+        try {
+            childDatastore = multiTenantCapableDatastore.getDatastoreForTenantId(tenantId)
+        } catch (Throwable e) {
+            // ignore
+        }
+        if (childDatastore != null && childDatastore.hasCurrentSession()) {
+            return CurrentTenantHolder.withTenant(multiTenantCapableDatastore, tenantId) {
+                def i = callable.parameterTypes.length
+                switch (i) {
+                    case 0:
+                        return callable.call()
+                    case 1:
+                        return callable.call(tenantId)
+                    case 2:
+                        return callable.call(tenantId, childDatastore.getCurrentSession())
+                    default:
+                        throw new IllegalArgumentException('Provided closure accepts too many arguments')
+                }
+            }
+        }
+        return CurrentTenantHolder.withTenant(multiTenantCapableDatastore, tenantId) {
             if (multiTenantCapableDatastore.getMultiTenancyMode().isSharedConnection()) {
                 def i = callable.parameterTypes.length
                 if (i == 2) {
                     return multiTenantCapableDatastore.withSession { session ->
-                        return callable.call(tenantId, session)
+                        def result = callable.call(tenantId, session)
+                        log.debug('Result from shared connection with 2 args: {}', result)
+                        return result
                     }
                 }
                 else {
                     switch (i) {
                         case 0:
-                            return callable.call()
-                            break
+                            def result = callable.call()
+                            log.debug('Result from shared connection with 0 args: {}', result)
+                            return result
                         case 1:
-                            return callable.call(tenantId)
-                            break
+                            def result = callable.call(tenantId)
+                            log.debug('Result from shared connection with 1 arg: {}', result)
+                            return result
                         default:
                             throw new IllegalArgumentException('Provided closure accepts too many arguments')
                     }
@@ -277,16 +335,21 @@ class Tenants {
             }
             else {
                 return multiTenantCapableDatastore.withNewSession(tenantId) { session ->
+                    log.debug('Inside withNewSession for tenantId {}', tenantId)
                     def i = callable.parameterTypes.length
                     switch (i) {
                         case 0:
-                            return callable.call()
-                            break
+                            def result = callable.call()
+                            log.debug('Result from new session with 0 args: {}', result)
+                            return result
                         case 1:
-                            return callable.call(tenantId)
-                            break
+                            def result = callable.call(tenantId)
+                            log.debug('Result from new session with 1 arg: {}', result)
+                            return result
                         case 2:
-                            return callable.call(tenantId, session)
+                            def result = callable.call(tenantId, session)
+                            log.debug('Result from new session with 2 args: {}', result)
+                            return result
                         default:
                             throw new IllegalArgumentException('Provided closure accepts too many arguments')
                     }
@@ -341,71 +404,4 @@ class Tenants {
         }
     }
 
-    @CompileStatic
-    protected static class CurrentTenant  {
-
-        private static final ThreadLocal<Serializable> currentTenantThreadLocal = new ThreadLocal<>()
-
-        /**
-         * @return Obtain the current tenant
-         */
-        static Serializable get() {
-            currentTenantThreadLocal.get()
-        }
-
-        /**
-         * Set the current tenant
-         *
-         * @param tenantId The tenant id
-         */
-        private static void set(Serializable tenantId) {
-            currentTenantThreadLocal.set(tenantId)
-        }
-
-        private static void remove() {
-            currentTenantThreadLocal.remove()
-        }
-
-        /**
-         * Execute with the current tenant
-         *
-         * @param callable The closure
-         * @return The result of the closure
-         */
-        static <T> T withTenant(Serializable tenantId, Closure<T> callable) {
-            def previous = get()
-            try {
-                set(tenantId)
-                callable.call(tenantId)
-            } finally {
-                if (previous == null) {
-                    remove()
-                }
-                else {
-                    set(previous)
-                }
-            }
-        }
-
-        /**
-         * Execute without current tenant
-         *
-         * @param callable The closure
-         * @return The result of the closure
-         */
-        static <T> T withoutTenant(Closure<T> callable) {
-            def previous = get()
-            try {
-                set(ConnectionSource.DEFAULT)
-                callable.call()
-            } finally {
-                if (previous == null) {
-                    remove()
-                } else {
-                    set(previous)
-                }
-            }
-        }
-    }
-
 }
diff --git a/grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy b/grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy
index 24592a175b..424aefd923 100644
--- a/grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy
+++ b/grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy
@@ -22,6 +22,8 @@ import groovy.transform.CompileStatic
 import groovy.transform.stc.ClosureParams
 import groovy.transform.stc.SimpleType
 
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 import org.springframework.transaction.PlatformTransactionManager
 import org.springframework.transaction.TransactionDefinition
 import org.springframework.transaction.TransactionException
@@ -43,6 +45,8 @@ import org.grails.datastore.mapping.transactions.CustomizableRollbackTransaction
 @CompileStatic
 class GrailsTransactionTemplate {
 
+    private static final Logger log = LoggerFactory.getLogger(GrailsTransactionTemplate)
+
     CustomizableRollbackTransactionAttribute transactionAttribute
 
     private org.springframework.transaction.support.TransactionTemplate transactionTemplate
@@ -69,12 +73,28 @@ class GrailsTransactionTemplate {
             Object result = transactionTemplate.execute(new TransactionCallback() {
                 Object doInTransaction(TransactionStatus status) {
                     try {
-                        return action.call(status)
+                        if (log.isDebugEnabled()) {
+                            log.debug('executeAndRollback doInTransaction - starting on transaction manager: ' + transactionTemplate.getTransactionManager())
+                        }
+                        Object val = action.call(status)
+                        if (log.isDebugEnabled()) {
+                            log.debug('executeAndRollback doInTransaction - finished action')
+                            log.debug('executeAndRollback action finished. status=' + status)
+                        }
+                        return val
                     }
                     catch (Throwable e) {
+                        if (log.isDebugEnabled()) {
+                            log.debug('executeAndRollback doInTransaction - caught exception: ' + e)
+                            log.debug('executeAndRollback action caught: ' + e)
+                        }
                         return new ThrowableHolder(e)
                     } finally {
                         status.setRollbackOnly()
+                        if (log.isDebugEnabled()) {
+                            log.debug('executeAndRollback doInTransaction - setRollbackOnly called. isRollbackOnly=' + status.isRollbackOnly())
+                            log.debug('executeAndRollback setRollbackOnly called. status.isRollbackOnly=' + status.isRollbackOnly())
+                        }
                     }
                 }
             })
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractDatastoreApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractDatastoreApi.groovy
index eb92f493fa..447c1361f3 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractDatastoreApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractDatastoreApi.groovy
@@ -31,30 +31,39 @@ import org.grails.datastore.mapping.core.VoidSessionCallback
 @CompileStatic
 abstract class AbstractDatastoreApi {
 
-    protected Datastore datastore
+    protected DatastoreResolver datastoreResolver
 
     protected AbstractDatastoreApi(Datastore datastore) {
-        this.datastore = datastore
+        this.datastoreResolver = new StaticDatastoreResolver(datastore)
+    }
+
+    protected AbstractDatastoreApi(DatastoreResolver datastoreResolver) {
+        this.datastoreResolver = datastoreResolver
     }
 
     protected <T> T execute(SessionCallback<T> callback) {
-        if (datastore == null) {
+        Datastore ds = getDatastore()
+        if (ds == null) {
             throw new IllegalStateException('Cannot execute session callback with null datastore')
         }
-        DatastoreUtils.execute(datastore, callback)
+        DatastoreUtils.execute(ds, callback)
     }
 
     protected void execute(VoidSessionCallback callback) {
-        if (datastore == null) {
+        Datastore ds = getDatastore()
+        if (ds == null) {
             throw new IllegalStateException('Cannot execute session callback with null datastore')
         }
-        DatastoreUtils.execute(datastore, callback)
+        DatastoreUtils.execute(ds, callback)
     }
 
     Datastore getDatastore() {
-        if (datastore == null) {
-            throw new IllegalStateException('No datastore configured in stateless mode')
-        }
-        return datastore
+        return datastoreResolver?.resolve()
+    }
+
+    private static class StaticDatastoreResolver implements DatastoreResolver {
+        private final Datastore datastore
+        StaticDatastoreResolver(Datastore datastore) { this.datastore = datastore }
+        @Override Datastore resolve() { datastore }
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
index c43d166546..a3bebc24cd 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy
@@ -20,76 +20,160 @@ package org.grails.datastore.gorm
 
 import java.lang.reflect.Method
 import java.lang.reflect.Modifier
+import java.util.concurrent.ConcurrentHashMap
 
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import grails.gorm.multitenancy.Tenants
 import org.grails.datastore.gorm.utils.ReflectionUtils
 import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.SessionCallback
+import org.grails.datastore.mapping.core.VoidSessionCallback
+import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
 
 /**
- * Abstract GORM API provider.
+ * Abstract base class for GORM API objects
  *
  * @author Graeme Rocher
- * @param <D> the entity/domain class
  * @since 1.0
  */
 @CompileStatic
 abstract class AbstractGormApi<D> extends AbstractDatastoreApi {
 
-    static final List<String> EXCLUDES = [
-        'setProperty',
-        'getProperty',
-        'getMetaClass',
-        'setMetaClass',
-        'invokeMethod',
-        'getMethods',
-        'getExtendedMethods',
-        'wait',
-        'equals',
-        'toString',
-        'hashCode',
-        'getClass',
-        'notify',
-        'notifyAll',
-        'setTransactionManager'
+    protected static final List<String> EXCLUDES = [
+        'wait', 'notify', 'notifyAll', 'toString', 'hashCode', 'equals', 'getClass',
+        'getMetaClass', 'setMetaClass', 'getProperty', 'setProperty', 'invokeMethod'
     ]
 
+    private static final Map<Class, List<Method>> METHODS_CACHE = new ConcurrentHashMap<>()
+    private static final Map<Class, List<Method>> EXTENDED_METHODS_CACHE = new ConcurrentHashMap<>()
+
     protected Class<D> persistentClass
-    protected PersistentEntity persistentEntity
+    protected final GormRegistry registry
+    protected final String qualifier
+    protected MappingContext mappingContext
     private List<Method> methods
     private List<Method> extendedMethods
 
     AbstractGormApi(Class<D> persistentClass, Datastore datastore) {
+        this(persistentClass, datastore, (GormRegistry) null)
+    }
+
+    AbstractGormApi(Class<D> persistentClass, Datastore datastore, GormRegistry registry) {
         super(datastore)
         this.persistentClass = persistentClass
-        this.persistentEntity = datastore.getMappingContext().getPersistentEntity(persistentClass.name)
+        this.registry = registry ?: GormRegistry.instance
+        this.qualifier = ConnectionSource.DEFAULT
+        this.mappingContext = datastore?.mappingContext
+    }
+
+    AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver) {
+        this(persistentClass, mappingContext, datastoreResolver, (String) null, (GormRegistry) null)
     }
 
-    AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext) {
-        super(null)
+    AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver, String qualifier, GormRegistry registry) {
+        super(datastoreResolver)
         this.persistentClass = persistentClass
-        this.persistentEntity = mappingContext.getPersistentEntity(persistentClass.name)
+        this.registry = registry ?: GormRegistry.instance
+        this.qualifier = qualifier ?: ConnectionSource.DEFAULT
+        this.mappingContext = mappingContext
     }
 
-    @CompileDynamic
-    protected initializeMethods(clazz) {
-        while (clazz != Object) {
-            final methodsToAdd = clazz.declaredMethods.findAll { Method m ->
-                def mods = m.getModifiers()
-                !m.isSynthetic() && !Modifier.isStatic(mods) && Modifier.isPublic(mods) &&
-                        !AbstractGormApi.EXCLUDES.contains(m.name)
+    @Override
+    protected <T1> T1 execute(SessionCallback<T1> callback) {
+        Datastore ds = getDatastore()
+        if (ds == null) {
+            throw new IllegalStateException('Cannot execute session callback with null datastore')
+        }
+
+        String currentQualifier = getQualifier()
+        boolean isMultiTenantCapable = ds instanceof MultiTenantCapableDatastore
+        boolean isMultiTenantEntity = MultiTenant.isAssignableFrom(persistentClass)
+
+        // Check if we have a non-default qualifier
+        if (currentQualifier != null && !ConnectionSource.DEFAULT.equals(currentQualifier) && !ConnectionSource.OLD_DEFAULT.equalsIgnoreCase(currentQualifier)) {
+            if (isMultiTenantEntity && isMultiTenantCapable) {
+                // If it's a multi-tenant entity and we have a qualifier, bind it as the tenant ID
+                return (T1) Tenants.withId((MultiTenantCapableDatastore)ds, (Serializable)currentQualifier) {
+                    DatastoreUtils.execute(ds, callback)
+                }
             }
-            methods.addAll(methodsToAdd)
-            if (clazz != GormStaticApi && clazz != GormInstanceApi && clazz != GormValidationApi && clazz != AbstractGormApi) {
-                def extendedMethodsToAdd = methodsToAdd.findAll { Method m -> !ReflectionUtils.isMethodOverriddenFromParent(m) }
-                extendedMethods.addAll(extendedMethodsToAdd)
+            return executeQualified(currentQualifier, callback)
+        }
+
+        // DEFAULT qualifier path: check if a tenant is already bound
+        if (isMultiTenantCapable) {
+            Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
+            if (tenantId != null) {
+                // If a tenant is already bound, use executeQualified to delegate to a potentially specialized API
+                return executeQualified(tenantId.toString(), callback)
             }
-            clazz = clazz.getSuperclass()
         }
-        return clazz
+
+        return DatastoreUtils.execute(ds, callback)
+    }
+
+    /**
+     * @return The qualifier for this API instance
+     */
+    String getQualifier() {
+        return this.qualifier
+    }
+
+    protected abstract <T1> T1 executeQualified(String qualifier, SessionCallback<T1> callback)
+
+    @Override
+    protected void execute(VoidSessionCallback callback) {
+        execute(new SessionCallback<Object>() {
+            @Override
+            Object doInSession(Session session) {
+                callback.doInSession(session)
+                return null
+            }
+        })
+    }
+
+    /**
+     * @return The persistent entity
+     */
+    PersistentEntity getGormPersistentEntity() {
+        getDatastore()?.mappingContext?.getPersistentEntity(persistentClass.name)
+    }
+
+    @CompileDynamic
+    protected synchronized void initializeMethods(Class apiClass) {
+        if (methods == null) {
+            if (!METHODS_CACHE.containsKey(apiClass)) {
+                List<Method> methodList = []
+                List<Method> extendedMethodList = []
+                Class cls = apiClass
+                while (cls != Object) {
+                    final methodsToAdd = cls.declaredMethods.findAll { Method m ->
+                        def mods = m.getModifiers()
+                        !m.isSynthetic() && !Modifier.isStatic(mods) && Modifier.isPublic(mods) &&
+                                !AbstractGormApi.EXCLUDES.contains(m.name)
+                    }
+                    methodList.addAll(methodsToAdd)
+                    if (cls != GormStaticApi && cls != GormInstanceApi && cls != GormValidationApi && cls != AbstractGormApi) {
+                        def extendedMethodsToAdd = methodsToAdd.findAll { Method m -> !ReflectionUtils.isMethodOverriddenFromParent(m) }
+                        extendedMethodList.addAll(extendedMethodsToAdd)
+                    }
+                    cls = cls.getSuperclass()
+                }
+                METHODS_CACHE.put(apiClass, Collections.unmodifiableList(methodList))
+                EXTENDED_METHODS_CACHE.put(apiClass, Collections.unmodifiableList(extendedMethodList))
+            }
+            this.methods = METHODS_CACHE.get(apiClass)
+            this.extendedMethods = EXTENDED_METHODS_CACHE.get(apiClass)
+        }
     }
 
     List<Method> getMethods() {
@@ -105,4 +189,19 @@ abstract class AbstractGormApi<D> extends AbstractDatastoreApi {
         }
         return extendedMethods
     }
+
+    abstract org.springframework.transaction.PlatformTransactionManager getTransactionManager()
+
+    static class ConstantDatastoreResolver implements DatastoreResolver {
+        private final Datastore datastore
+
+        ConstantDatastoreResolver(Datastore datastore) {
+            this.datastore = datastore
+        }
+
+        @Override
+        Datastore resolve() {
+            return datastore
+        }
+    }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApiRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApiRegistry.groovy
new file mode 100644
index 0000000000..2ac057c9d9
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApiRegistry.groovy
@@ -0,0 +1,152 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  'License'); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+
+import java.util.concurrent.ConcurrentHashMap
+
+@CompileStatic
+abstract class AbstractGormApiRegistry<T extends AbstractDatastoreApi> {
+
+    private final Map<String, T> apis = new ConcurrentHashMap<>()
+    private final Map<String, Map<String, T>> qualifiedApis = new ConcurrentHashMap<>()
+    protected final GormRegistry registry
+
+    AbstractGormApiRegistry(GormRegistry registry) {
+        this.registry = registry
+    }
+
+    void register(String className, T api) {
+        String normalizedClassName = registry.normalizeEntityKey(className)
+        if (normalizedClassName != null && api != null) {
+            apis.put(normalizedClassName, api)
+            qualifiedApis.remove(normalizedClassName)
+        }
+    }
+
+    T get(String className) {
+        return apis.get(registry.normalizeEntityKey(className))
+    }
+
+    T get(String className, String qualifier) {
+        return getDirect(registry.normalizeEntityKey(className), registry.normalizeQualifier(qualifier))
+    }
+
+    T getDirect(String normalizedClassName, String normalizedQualifier) {
+        T defaultApi = apis.get(normalizedClassName)
+        if (defaultApi == null) {
+            return null
+        }
+
+        Datastore ds = registry.getDatastoreDirect(normalizedClassName, normalizedQualifier)
+        if (ds == null && defaultApi.getDatastore() instanceof MultipleConnectionSourceCapableDatastore) {
+            Datastore defaultDatastore = defaultApi.getDatastore()
+            boolean canResolveConnection = true
+            if (defaultDatastore instanceof MultiTenantCapableDatastore) {
+                MultiTenancySettings.MultiTenancyMode mode = ((MultiTenantCapableDatastore) defaultDatastore).getMultiTenancyMode()
+                if (mode == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR ||
+                        mode == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
+                    canResolveConnection = false
+                }
+            }
+            if (canResolveConnection) {
+                ds = ((MultipleConnectionSourceCapableDatastore) defaultDatastore).getDatastoreForConnection(normalizedQualifier)
+            } else {
+                ds = defaultDatastore
+            }
+        }
+
+        if (ds != null && ds != defaultApi.getDatastore()) {
+            Map<String, T> classQualifiedApis = qualifiedApis.computeIfAbsent(normalizedClassName, { new ConcurrentHashMap<String, T>() })
+            T api = classQualifiedApis.get(normalizedQualifier)
+            if (api == null) {
+                api = qualify(defaultApi, normalizedQualifier)
+                if (api != null) {
+                    classQualifiedApis.put(normalizedQualifier, api)
+                }
+            }
+            return api
+        }
+
+        return defaultApi
+    }
+
+    boolean containsKey(String className) {
+        return apis.containsKey(registry.normalizeEntityKey(className))
+    }
+
+    int size() {
+        return apis.size()
+    }
+
+    Set<String> keySet() {
+        return apis.keySet()
+    }
+
+    void clear() {
+        apis.clear()
+        qualifiedApis.clear()
+    }
+
+    void removeDatastore(Datastore datastore) {
+        if (datastore == null) return
+        Iterator<Map.Entry<String, T>> it = apis.entrySet().iterator()
+        while (it.hasNext()) {
+            try {
+                if (it.next().value.getDatastore() == datastore) {
+                    it.remove()
+                }
+            } catch (Exception e) {
+                it.remove()
+            }
+        }
+        Iterator<Map.Entry<String, Map<String, T>>> qit = qualifiedApis.entrySet().iterator()
+        while (qit.hasNext()) {
+            Map<String, T> classQualifiedApis = qit.next().value
+            Iterator<Map.Entry<String, T>> eit = classQualifiedApis.entrySet().iterator()
+            while (eit.hasNext()) {
+                try {
+                    if (eit.next().value.getDatastore() == datastore) {
+                        eit.remove()
+                    }
+                } catch (Exception e) {
+                    eit.remove()
+                }
+            }
+            if (classQualifiedApis.isEmpty()) {
+                qit.remove()
+            }
+        }
+    }
+
+    protected String className(Class entity) {
+        return registry.normalizeEntityKey(entity)
+    }
+
+    protected IllegalStateException stateException(Class entity) {
+        return new IllegalStateException("No GORM implementation configured for class [${entity.name}]. Ensure GORM has been initialized correctly")
+    }
+
+    protected abstract T qualify(T api, String qualifier)
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/ConnectionSourceNameResolver.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/ConnectionSourceNameResolver.groovy
new file mode 100644
index 0000000000..bad0e2eda0
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/ConnectionSourceNameResolver.groovy
@@ -0,0 +1,70 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.ConnectionSources
+import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
+
+/**
+ * Resolves connection source names from a datastore.
+ *
+ * @author Graeme Rocher
+ */
+@CompileStatic
+class ConnectionSourceNameResolver {
+
+    /**
+     * Resolve all connection source names from a datastore.
+     * Returns a list of connection source names, or defaults to [ConnectionSource.DEFAULT] if none found.
+     *
+     * @param datastore The datastore to resolve names from
+     * @return List of connection source names
+     */
+    static List<String> resolveConnectionSourceNames(Object datastore) {
+        if (datastore instanceof ConnectionSourcesProvider) {
+            ConnectionSources connectionSources = ((ConnectionSourcesProvider) datastore).connectionSources
+            if (connectionSources != null) {
+                Iterable<ConnectionSource> allConnections = connectionSources.allConnectionSources
+                if (allConnections instanceof Collection) {
+                    List<String> names = ((Collection<ConnectionSource>) allConnections).collect { it.name }
+                    return names.isEmpty() ? [ConnectionSource.DEFAULT] : names
+                } else {
+                    return allConnections?.collect { it.name } ?: [ConnectionSource.DEFAULT]
+                }
+            }
+        }
+        return [ConnectionSource.DEFAULT]
+    }
+
+    /**
+     * Resolve the default connection source name from a datastore.
+     * Returns the default connection source name, or ConnectionSource.DEFAULT if none found.
+     *
+     * @param datastore The datastore to resolve the name from
+     * @return The default connection source name
+     */
+    static String resolveDefaultConnectionSourceName(Object datastore) {
+        if (datastore instanceof ConnectionSourcesProvider) {
+            return ((ConnectionSourcesProvider) datastore).connectionSources?.defaultConnectionSource?.name ?: ConnectionSource.DEFAULT
+        }
+        return ConnectionSource.DEFAULT
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DatastoreResolver.groovy
similarity index 58%
copy from grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java
copy to grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DatastoreResolver.groovy
index 5e8ee5cf61..1e318298e9 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DatastoreResolver.groovy
@@ -16,25 +16,24 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.grails.datastore.gorm.finders;
+package org.grails.datastore.gorm
 
-import java.util.regex.Pattern;
+import groovy.transform.CompileStatic
 
-import org.grails.datastore.mapping.core.Datastore;
-import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.core.Datastore
 
 /**
- * Finder used to return a single result
+ * Strategy interface for resolving a Datastore at call-time.
+ * This breaks the circular dependency between API objects and GormEnhancer.
+ *
+ * @author Walter Duque de Estrada
+ * @since 8.0.0
  */
-public class FindByFinder extends AbstractFindByFinder {
-
-    private static final String METHOD_PATTERN = "(findBy)([A-Z]\\w*)";
-
-    public FindByFinder(final Datastore datastore) {
-        super(Pattern.compile(METHOD_PATTERN), datastore);
-    }
+@CompileStatic
+interface DatastoreResolver {
 
-    public FindByFinder(MappingContext mappingContext) {
-        super(Pattern.compile(METHOD_PATTERN), mappingContext);
-    }
+    /**
+     * @return The datastore to use for the current call
+     */
+    Datastore resolve()
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DefaultGormApiFactory.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DefaultGormApiFactory.groovy
new file mode 100644
index 0000000000..331f887e09
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/DefaultGormApiFactory.groovy
@@ -0,0 +1,83 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.gorm.finders.CountByFinder
+import org.grails.datastore.gorm.finders.FindAllByBooleanFinder
+import org.grails.datastore.gorm.finders.FindAllByFinder
+import org.grails.datastore.gorm.finders.FindByBooleanFinder
+import org.grails.datastore.gorm.finders.FindByFinder
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.gorm.finders.FindOrCreateByFinder
+import org.grails.datastore.gorm.finders.FindOrSaveByFinder
+import org.grails.datastore.gorm.finders.ListOrderByFinder
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * Default core factory for GORM API object creation.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class DefaultGormApiFactory implements GormApiFactory {
+
+    @Override
+    <D> GormStaticApi<D> createStaticApi(Class<D> persistentClass,
+                                         MappingContext mappingContext,
+                                         DatastoreResolver resolver,
+                                         String qualifier,
+                                         GormRegistry registry) {
+        List<FinderMethod> finders = createDynamicFinders(resolver, mappingContext)
+        return new GormStaticApi<D>(persistentClass, mappingContext, finders, resolver, qualifier, registry)
+    }
+
+    @Override
+    <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass,
+                                             MappingContext mappingContext,
+                                             DatastoreResolver resolver,
+                                             GormRegistry registry,
+                                             boolean failOnError,
+                                             boolean markDirty) {
+        GormInstanceApi<D> instanceApi = new GormInstanceApi<D>(persistentClass, mappingContext, resolver, registry)
+        instanceApi.failOnError = failOnError
+        instanceApi.markDirty = markDirty
+        return instanceApi
+    }
+
+    @Override
+    <D> GormValidationApi<D> createValidationApi(Class<D> persistentClass,
+                                                 MappingContext mappingContext,
+                                                 DatastoreResolver resolver,
+                                                 GormRegistry registry) {
+        return new GormValidationApi<D>(persistentClass, mappingContext, resolver, registry)
+    }
+
+    @Override
+    List<FinderMethod> createDynamicFinders(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        [new FindOrCreateByFinder(datastoreResolver, mappingContext),
+         new FindOrSaveByFinder(datastoreResolver, mappingContext),
+         new FindByFinder(datastoreResolver, mappingContext),
+         new FindAllByFinder(datastoreResolver, mappingContext),
+         new FindAllByBooleanFinder(datastoreResolver, mappingContext),
+         new FindByBooleanFinder(datastoreResolver, mappingContext),
+         new CountByFinder(datastoreResolver, mappingContext),
+         new ListOrderByFinder(datastoreResolver, mappingContext)] as List<FinderMethod>
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiFactory.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiFactory.groovy
new file mode 100644
index 0000000000..7f085b97fe
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiFactory.groovy
@@ -0,0 +1,52 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * Abstract factory for creating GORM API instances.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+interface GormApiFactory {
+
+    <D> GormStaticApi<D> createStaticApi(Class<D> persistentClass,
+                                         MappingContext mappingContext,
+                                         DatastoreResolver resolver,
+                                         String qualifier,
+                                         GormRegistry registry)
+
+    <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass,
+                                             MappingContext mappingContext,
+                                             DatastoreResolver resolver,
+                                             GormRegistry registry,
+                                             boolean failOnError,
+                                             boolean markDirty)
+
+    <D> GormValidationApi<D> createValidationApi(Class<D> persistentClass,
+                                                 MappingContext mappingContext,
+                                                 DatastoreResolver resolver,
+                                                 GormRegistry registry)
+
+    List<FinderMethod> createDynamicFinders(DatastoreResolver datastoreResolver, MappingContext mappingContext)
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiResolver.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiResolver.groovy
new file mode 100644
index 0000000000..36aeed8b03
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormApiResolver.groovy
@@ -0,0 +1,414 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import grails.gorm.multitenancy.Tenants
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.reflect.NameUtils
+import org.springframework.transaction.support.TransactionSynchronizationManager
+
+/**
+ * Instance-based resolver for GORM APIs and datastores.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class GormApiResolver {
+
+    private final GormRegistry registry
+    private final GormEnhancerRegistry stateRegistry = GormEnhancerRegistry.getInstance()
+    private final PreferredDatastoreSelector preferredDatastoreSelector
+    private final QualifiedDatastoreSelector qualifiedDatastoreSelector
+    private final ActiveSessionDatastoreSelector activeSessionDatastoreSelector
+    private final DefaultDatastoreSelector defaultDatastoreSelector
+
+    GormApiResolver(GormRegistry registry) {
+        this.registry = registry
+        this.preferredDatastoreSelector = new PreferredDatastoreSelector()
+        this.qualifiedDatastoreSelector = new QualifiedDatastoreSelector()
+        this.activeSessionDatastoreSelector = new ActiveSessionDatastoreSelector()
+        this.defaultDatastoreSelector = new DefaultDatastoreSelector()
+    }
+
+    @CompileDynamic
+    Datastore findDatastore(Class entity, String qualifier = null) {
+        int depth = stateRegistry.getResolvingDatastoreDepth()
+        if (depth > 5) {
+            return registry.datastoresByQualifier.get(ConnectionSource.DEFAULT)
+        }
+
+        String className = entity != null ? NameUtils.getClassName(entity) : null
+
+        Datastore selected = preferredDatastoreSelector.select(registry, stateRegistry, entity, qualifier, className, depth, this)
+        if (selected != null) {
+            return selected
+        }
+
+        if (qualifier != null && !ConnectionSource.DEFAULT.equals(qualifier)) {
+            return qualifiedDatastoreSelector.select(registry, stateRegistry, className, qualifier, depth)
+        }
+
+        selected = activeSessionDatastoreSelector.select(registry, className)
+        if (selected != null) {
+            return selected
+        }
+
+        Datastore defaultDs = defaultDatastoreSelector.select(registry, stateRegistry, entity, className, depth, this)
+
+        if (defaultDs == null) {
+            defaultDs = registry.getDatastore((Class) null, ConnectionSource.DEFAULT)
+        }
+        if (defaultDs == null && entity != null) {
+            throw stateException(entity)
+        }
+        return defaultDs
+    }
+
+    Datastore findDatastoreByType(Class<? extends Datastore> datastoreType) {
+        Datastore datastore = registry.datastoresByType.get(datastoreType)
+        if (datastore == null) {
+            for (entry in registry.datastoresByType.entrySet()) {
+                if (datastoreType.isAssignableFrom(entry.key)) {
+                    datastore = entry.value
+                    break
+                }
+            }
+        }
+        if (datastore == null) {
+            throw new IllegalStateException("No GORM implementation configured for type [$datastoreType]. Ensure GORM has been initialized correctly")
+        }
+        return datastore
+    }
+
+    Datastore findSingleDatastore() {
+        if (registry.datastoresByQualifier.size() > 1) {
+            return findDatastore(null, null)
+        }
+
+        Datastore defaultDs = registry.datastoresByQualifier.get(ConnectionSource.DEFAULT)
+        if (defaultDs != null) {
+            return defaultDs
+        }
+
+        if (registry.datastoresByQualifier.size() == 1) {
+            return registry.datastoresByQualifier.values().first()
+        }
+
+        Collection<Datastore> allDatastores = registry.datastoresByType.values()
+        if (allDatastores.isEmpty()) {
+            throw new IllegalStateException('No GORM implementations configured. Ensure GORM has been initialized correctly')
+        }
+        if (allDatastores.size() > 1) {
+            throw new IllegalStateException("More than one GORM implementation is configured. Registered by type: ${allDatastores*.getClass()*.name}. Registered by qualifier: ${registry.datastoresByQualifier.keySet()}")
+        }
+        return allDatastores.first()
+    }
+
+    PersistentEntity findEntity(Class entity, String qualifier = null) {
+        String resolvedQualifier = qualifier ?: findTenantId(entity)
+        return findDatastore(entity, resolvedQualifier)?.mappingContext?.getPersistentEntity(entity.name)
+    }
+
+    private String findTenantId(Class entity) {
+        if (entity != null && MultiTenant.isAssignableFrom(entity)) {
+            Datastore defaultDatastore = registry.getDatastoreByString(entity.name, ConnectionSource.DEFAULT)
+            if (defaultDatastore instanceof MultiTenantCapableDatastore) {
+                MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) defaultDatastore
+                try {
+                    Serializable tid = Tenants.currentId(multiTenantCapableDatastore)
+                    return tid?.toString() ?: ConnectionSource.DEFAULT
+                } catch (Throwable e) {
+                    return ConnectionSource.DEFAULT
+                }
+            }
+        }
+        return ConnectionSource.DEFAULT
+    }
+
+    private IllegalStateException stateException(Class entity) {
+        return new IllegalStateException("No GORM implementation configured for class [${entity.name}]. Ensure GORM has been initialized correctly")
+    }
+
+}
+
+@CompileStatic
+class PreferredDatastoreSelector {
+
+    @CompileDynamic
+    Datastore select(GormRegistry registry, GormEnhancerRegistry stateRegistry, Class entity, String qualifier, String className, int depth, GormApiResolver resolver) {
+        Datastore preferred = stateRegistry.getPreferredDatastore()
+        if (preferred == null) {
+            return null
+        }
+        if (className != null) {
+            if (preferred.mappingContext?.getPersistentEntity(className) == null) {
+                return null
+            }
+            Datastore defaultDs = registry.getDatastore(className, ConnectionSource.DEFAULT)
+            if (defaultDs != null && defaultDs != preferred && !registry.isDatastoreRegisteredForEntity(className, preferred)) {
+                return null
+            }
+        }
+
+        boolean isDefaultQualifier = qualifier == null || ConnectionSource.DEFAULT.equals(qualifier)
+        if (isDefaultQualifier) {
+            if (preferred instanceof MultiTenantCapableDatastore) {
+                MultiTenantCapableDatastore mtds = (MultiTenantCapableDatastore) preferred
+                try {
+                    Serializable tid = CurrentTenantHolder.get()
+                    if (tid == null && entity != null && MultiTenant.isAssignableFrom(entity)) {
+                        tid = mtds.tenantResolver.resolveTenantIdentifier()
+                    }
+                    if (tid != null && !ConnectionSource.DEFAULT.equals(tid.toString())) {
+                        stateRegistry.setResolvingDatastoreDepth(depth + 1)
+                        try {
+                            return resolver.findDatastore(entity, tid.toString())
+                        } finally {
+                            stateRegistry.setResolvingDatastoreDepth(depth)
+                        }
+                    }
+                } catch (Throwable e) {
+                    if (entity != null && MultiTenant.isAssignableFrom(entity) && e instanceof TenantNotFoundException) {
+                        throw e
+                    }
+                }
+            }
+            return preferred
+        }
+
+        if (preferred instanceof MultipleConnectionSourceCapableDatastore) {
+            try {
+                Datastore ds = ((MultipleConnectionSourceCapableDatastore) preferred).getDatastoreForConnection(qualifier)
+                if (ds != null) {
+                    return ds
+                }
+            } catch (Throwable e) {
+                // ignore
+            }
+        }
+        return null
+    }
+}
+
+@CompileStatic
+class QualifiedDatastoreSelector {
+
+    @CompileDynamic
+    Datastore select(GormRegistry registry, GormEnhancerRegistry stateRegistry, String className, String qualifier, int depth) {
+        Object resource = TransactionSynchronizationManager.getResource(qualifier)
+        if (resource instanceof Datastore) {
+            return (Datastore) resource
+        }
+
+        // Check the entity-specific datastore map first. For SCHEMA mode, tenant IDs ARE connection
+        // names (each tenant has its own child datastore registered here). For DISCRIMINATOR mode
+        // with an explicit datasource mapping (e.g. datasource 'analytics'), the analytics child is
+        // also registered here and must be returned directly.
+        Datastore ds = registry.getDatastoreByString(className, qualifier)
+        if (ds != null) {
+            return ds
+        }
+
+        Datastore defaultDs = registry.getDatastoreByString(className, ConnectionSource.DEFAULT)
+        // For DISCRIMINATOR mode: the qualifier is a logical tenant ID, not a datasource connection
+        // name. Discriminator switching happens at the Hibernate session/filter level, so the parent
+        // datastore must be returned. (SCHEMA tenants are already handled above via the entity map.)
+        if (defaultDs instanceof MultiTenantCapableDatastore) {
+            MultiTenancySettings.MultiTenancyMode mode = ((MultiTenantCapableDatastore) defaultDs).getMultiTenancyMode()
+            if (mode == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) {
+                return defaultDs
+            }
+        }
+
+        if (defaultDs instanceof MultipleConnectionSourceCapableDatastore) {
+            try {
+                stateRegistry.setResolvingDatastoreDepth(depth + 1)
+                ds = ((MultipleConnectionSourceCapableDatastore) defaultDs).getDatastoreForConnection(qualifier)
+                if (ds != null && ds != defaultDs) {
+                    return ds
+                }
+            } catch (Throwable e) {
+                // ignore
+            } finally {
+                stateRegistry.setResolvingDatastoreDepth(depth)
+            }
+        }
+        if (defaultDs instanceof MultiTenantCapableDatastore) {
+            try {
+                stateRegistry.setResolvingDatastoreDepth(depth + 1)
+                ds = ((MultiTenantCapableDatastore) defaultDs).getDatastoreForTenantId(qualifier)
+                if (ds != null && ds != defaultDs) {
+                    return ds
+                }
+            } catch (Throwable e) {
+                // ignore
+            } finally {
+                stateRegistry.setResolvingDatastoreDepth(depth)
+            }
+        }
+        return defaultDs
+    }
+}
+
+@CompileStatic
+class ActiveSessionDatastoreSelector {
+
+    @CompileDynamic
+    Datastore select(GormRegistry registry, String className) {
+        // Optimization: Use TransactionSynchronizationManager.getResourceMap() to only check datastores with active sessions in the current thread.
+        // This avoids O(M) iteration over all registered datastores (which can be thousands in multi-tenancy).
+        Map resourceMap = TransactionSynchronizationManager.getResourceMap()
+        if (resourceMap != null && !resourceMap.isEmpty()) {
+            for (Object key : resourceMap.keySet()) {
+                if (key instanceof Datastore) {
+                    Datastore ds = (Datastore) key
+                    if (!ds.hasCurrentSession()) {
+                        continue
+                    }
+                    if (className != null) {
+                        Datastore defaultDs = registry.getDatastore(className, ConnectionSource.DEFAULT)
+                        if (defaultDs == ds || registry.isDatastoreRegisteredForEntity(className, ds)) {
+                            if (shouldSkipActiveDatastore(ds, className, defaultDs)) {
+                                continue
+                            }
+                            return ds
+                        }
+                    } else {
+                        if (shouldSkipActiveDatastore(ds, null, null)) {
+                            continue
+                        }
+                        return ds
+                    }
+                }
+            }
+        }
+        
+        // Fallback: If no datastore found in TransactionSynchronizationManager, 
+        // we might still have a non-transactional session bound to a ThreadLocalSessionResolver.
+        // For performance, we only do the full iteration if allDatastores is small.
+        if (registry.allDatastores.size() <= 10) {
+            for (Datastore registeredDs in registry.allDatastores) {
+                if (registeredDs.hasCurrentSession()) {
+                    if (className != null) {
+                        Datastore defaultDs = registry.getDatastore(className, ConnectionSource.DEFAULT)
+                        if (defaultDs == registeredDs || registry.isDatastoreRegisteredForEntity(className, registeredDs)) {
+                            if (shouldSkipActiveDatastore(registeredDs, className, defaultDs)) {
+                                continue
+                            }
+                            return registeredDs
+                        }
+                    } else if (registry.allDatastores.size() == 1) {
+                        if (shouldSkipActiveDatastore(registeredDs, null, null)) {
+                            continue
+                        }
+                        return registeredDs
+                    }
+                }
+            }
+        }
+        return null
+    }
+
+    private boolean shouldSkipActiveDatastore(Datastore ds, String className, Datastore defaultDs) {
+        if (ds instanceof MultiTenantCapableDatastore) {
+            MultiTenancySettings.MultiTenancyMode mode = ((MultiTenantCapableDatastore) ds).getMultiTenancyMode()
+            if (mode == MultiTenancySettings.MultiTenancyMode.DATABASE || mode == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
+                if (className != null) {
+                    PersistentEntity entity = ds.getMappingContext().getPersistentEntity(className)
+                    if (entity != null && entity.isMultiTenant()) {
+                        Serializable resolvedTenantId = null
+                        try {
+                            resolvedTenantId = CurrentTenantHolder.get()
+                            if (resolvedTenantId == null) {
+                                resolvedTenantId = ((MultiTenantCapableDatastore) ds).getTenantResolver().resolveTenantIdentifier()
+                            }
+                        } catch (TenantNotFoundException e) {
+                            return true
+                        }
+                        if (resolvedTenantId == null) {
+                            return true
+                        }
+                        String activeConnectionName = ds.getConnectionSources().getDefaultConnectionSource().getName()
+                        if (ConnectionSource.DEFAULT.equals(activeConnectionName)) {
+                            return true
+                        }
+                        if (!activeConnectionName.equals(resolvedTenantId.toString())) {
+                            return true
+                        }
+                    }
+                } else {
+                    if (ConnectionSource.DEFAULT.equals(ds.getConnectionSources().getDefaultConnectionSource().getName())) {
+                        return true
+                    }
+                }
+            }
+        }
+        return false
+    }
+}
+
+@CompileStatic
+class DefaultDatastoreSelector {
+
+    @CompileDynamic
+    Datastore select(GormRegistry registry, GormEnhancerRegistry stateRegistry, Class entity, String className, int depth, GormApiResolver resolver) {
+        Datastore defaultDs = registry.getDatastoreByString(className, ConnectionSource.DEFAULT)
+        if (defaultDs instanceof MultiTenantCapableDatastore) {
+            MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) defaultDs
+            boolean isDatabaseMode = multiTenantCapableDatastore.getMultiTenancyMode() ==
+                    MultiTenancySettings.MultiTenancyMode.DATABASE
+            try {
+                Serializable currentTenantId = CurrentTenantHolder.get()
+                if (currentTenantId == null && entity != null && MultiTenant.isAssignableFrom(entity)) {
+                    currentTenantId = multiTenantCapableDatastore.tenantResolver.resolveTenantIdentifier()
+                }
+
+                if (ConnectionSource.DEFAULT.equals(currentTenantId)) {
+                    return defaultDs
+                }
+
+                if (currentTenantId != null && !ConnectionSource.DEFAULT.equals(currentTenantId.toString())) {
+                    stateRegistry.setResolvingDatastoreDepth(depth + 1)
+                    try {
+                        return resolver.findDatastore(entity, currentTenantId.toString())
+                    } finally {
+                        stateRegistry.setResolvingDatastoreDepth(depth)
+                    }
+                }
+            } catch (Throwable e) {
+                if (entity != null && MultiTenant.isAssignableFrom(entity) && e instanceof TenantNotFoundException) {
+                    if (isDatabaseMode || multiTenantCapableDatastore.getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
+                        throw e
+                    }
+                }
+            }
+        }
+        return defaultDs
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
index efadd5c1cc..a8cbd4f690 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
@@ -1,131 +1,94 @@
-/* Copyright (C) 2010-2025 the original author or authors.
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
  *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ *    https://www.apache.org/licenses/LICENSE-2.0
  *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
  */
 package org.grails.datastore.gorm
 
-import java.lang.reflect.Method
-import java.lang.reflect.Modifier
-import java.util.concurrent.ConcurrentHashMap
-
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
-import org.codehaus.groovy.reflection.CachedMethod
-import org.codehaus.groovy.runtime.metaclass.ClosureStaticMetaMethod
-import org.codehaus.groovy.runtime.metaclass.MethodSelectionException
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 
 import org.springframework.transaction.PlatformTransactionManager
-import org.springframework.transaction.TransactionSystemException
 
 import grails.gorm.MultiTenant
-import grails.gorm.multitenancy.Tenants
-import org.grails.datastore.gorm.finders.CountByFinder
-import org.grails.datastore.gorm.finders.FindAllByBooleanFinder
-import org.grails.datastore.gorm.finders.FindAllByFinder
-import org.grails.datastore.gorm.finders.FindByBooleanFinder
-import org.grails.datastore.gorm.finders.FindByFinder
-import org.grails.datastore.gorm.finders.FindOrCreateByFinder
-import org.grails.datastore.gorm.finders.FindOrSaveByFinder
-import org.grails.datastore.gorm.finders.FinderMethod
-import org.grails.datastore.gorm.finders.ListOrderByFinder
-import org.grails.datastore.gorm.internal.InstanceMethodInvokingClosure
-import org.grails.datastore.gorm.internal.StaticMethodInvokingClosure
 import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.core.connections.ConnectionSource
 import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
-import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
 import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport
-import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
 import org.grails.datastore.mapping.model.PersistentEntity
-import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
-import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
-import org.grails.datastore.mapping.reflect.ClassUtils
 import org.grails.datastore.mapping.reflect.MetaClassUtils
-import org.grails.datastore.mapping.reflect.NameUtils
-import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
 /**
- * Enhances a class with GORM behavior
+ * Enhances a class with GORM methods
  *
  * @author Graeme Rocher
  */
-@Slf4j
 @CompileStatic
 class GormEnhancer implements Closeable {
 
-    private static final Map<String, Map<String, Closure>> NAMED_QUERIES = new ConcurrentHashMap<>()
-
-    private static final Map<String, Map<String, GormStaticApi>> STATIC_APIS = new ConcurrentHashMap<String, Map<String, GormStaticApi>>().withDefault { String key ->
-        return new ConcurrentHashMap() as Map<String, GormStaticApi>
-    }
-    private static final Map<String, Map<String, GormInstanceApi>> INSTANCE_APIS = new ConcurrentHashMap<String, Map<String, GormInstanceApi>>().withDefault { String key ->
-        return new ConcurrentHashMap() as Map<String, GormInstanceApi>
-    }
-    private static final Map<String, Map<String, GormValidationApi>> VALIDATION_APIS = new ConcurrentHashMap<String, Map<String, GormValidationApi>>().withDefault { String key ->
-        return new ConcurrentHashMap() as Map<String, GormValidationApi>
-    }
-    private static final Map<String, Map<String, Datastore>> DATASTORES = new ConcurrentHashMap<String, Map<String, Datastore>>().withDefault { String key ->
-        return new ConcurrentHashMap() as Map<String, Datastore>
-    }
-
-    private static final Map<Datastore, GormEnhancer> ENHANCERS = new ConcurrentHashMap<>()
-
-    private static final Map<Class, Datastore> DATASTORES_BY_TYPE = new ConcurrentHashMap<Class, Datastore>()
+    private static final Logger log = LoggerFactory.getLogger(GormEnhancer)
+    private static final GormEnhancerRegistry STATE_REGISTRY = GormEnhancerRegistry.getInstance()
 
+    private final GormRegistry registry
+    private final List<String> connectionSourceNames
     final Datastore datastore
-    PlatformTransactionManager transactionManager
-    List<FinderMethod> finders
-    boolean failOnError
-    boolean markDirty
+    boolean failOnError = false
+    boolean markDirty = true
 
     /**
      * Whether to include external entities
      */
     boolean includeExternal = true
+
     /**
-     * Whether to enhance classes dynamically using meta programming as well, only necessary for Java classes
+     * Backward-compatible constructor for callers that pass failOnError as a boolean.
      */
-    final boolean dynamicEnhance
-
-    GormEnhancer(Datastore datastore) {
-        this(datastore, null)
-    }
-
-    GormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, boolean failOnError = false, boolean dynamicEnhance = false, boolean markDirty = true) {
-        this(datastore, transactionManager, new ConnectionSourceSettings().failOnError(failOnError).markDirty(markDirty))
+    GormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, boolean failOnError) {
+        this(datastore, transactionManager, new ConnectionSourceSettings().failOnError(failOnError))
     }
 
     /**
-     * Construct a new GormEnhancer for the given arguments
+     * Construct a new GormEnhancer for the given arguments.
      *
-     * @param datastore The datastore
-     * @param transactionManager The transaction manager
-     * @param settings The settings
+     * @param datastore The datastore (required)
+     * @param transactionManager Retained for constructor compatibility
+     * @param settings The connection source settings (required)
+     * @param registry The GORM registry (optional, defaults to singleton instance)
      */
-    GormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings) {
+    GormEnhancer(Datastore datastore,
+                 PlatformTransactionManager ignoredTransactionManager,
+                 ConnectionSourceSettings settings,
+                 GormRegistry registry = GormRegistry.getInstance()) {
+        assert datastore != null, 'Datastore is required'
+        assert settings != null, 'ConnectionSourceSettings is required'
+        
         this.datastore = datastore
+        this.registry = registry
+        
         this.failOnError = settings.isFailOnError()
         Boolean markDirty = settings.getMarkDirty()
         this.markDirty = markDirty == null ? true : markDirty
-        this.transactionManager = transactionManager
-        this.dynamicEnhance = false
-        if (datastore != null) {
-            registerConstraints(datastore)
-        }
-        NAMED_QUERIES.clear()
-        ENHANCERS.put(datastore, this)
-        DATASTORES_BY_TYPE.put(datastore.getClass(), datastore)
+
+        this.connectionSourceNames = ConnectionSourceNameResolver.resolveConnectionSourceNames(datastore)
+
+        String qualifier = ConnectionSourceNameResolver.resolveDefaultConnectionSourceName(datastore)
+        registry.initializeDatastore(datastore, qualifier)
 
         for (entity in datastore.mappingContext.persistentEntities) {
             registerEntity(entity)
@@ -138,73 +101,16 @@ class GormEnhancer implements Closeable {
      * @param entity The entity
      */
     void registerEntity(PersistentEntity entity) {
-        Datastore datastore = this.datastore
-        if (appliesToDatastore(datastore, entity)) {
-            def cls = entity.javaClass
-
-            List<String> qualifiers = allQualifiers(this.datastore, entity)
-            List<String> apiQualifiers = apiQualifiers(entity, qualifiers)
-            def name = entity.name
-
-            if (!apiQualifiers.contains(ConnectionSource.DEFAULT)) {
-                def firstQualifier = apiQualifiers.first()
-                def staticApi = getStaticApi(cls, firstQualifier)
-                STATIC_APIS.get(ConnectionSource.DEFAULT).put(name, staticApi)
-                def instanceApi = getInstanceApi(cls, firstQualifier)
-                INSTANCE_APIS.get(ConnectionSource.DEFAULT).put(name, instanceApi)
-                def validationApi = getValidationApi(cls, firstQualifier)
-                VALIDATION_APIS.get(ConnectionSource.DEFAULT).put(name, validationApi)
-                DATASTORES.get(ConnectionSource.DEFAULT).put(name, this.datastore)
-
-            }
-            for (qualifier in apiQualifiers) {
-                def staticApi = getStaticApi(cls, qualifier)
-                STATIC_APIS.get(qualifier).put(name, staticApi)
-                def instanceApi = getInstanceApi(cls, qualifier)
-                INSTANCE_APIS.get(qualifier).put(name, instanceApi)
-                def validationApi = getValidationApi(cls, qualifier)
-                VALIDATION_APIS.get(qualifier).put(name, validationApi)
-            }
-            for (qualifier in qualifiers) {
-                DATASTORES.get(qualifier).put(name, this.datastore)
-                // Evict any lazily-cached API for this qualifier so the next access
-                // re-creates it against the now-current datastore/session-factory.
-                // Required when addTenantForSchema recreates a child datastore (e.g.
-                // per-test schema setup) and this qualifier was not in apiQualifiers.
-                if (!apiQualifiers.contains(qualifier)) {
-                    STATIC_APIS.get(qualifier)?.remove(name)
-                    INSTANCE_APIS.get(qualifier)?.remove(name)
-                    VALIDATION_APIS.get(qualifier)?.remove(name)
-                }
-            }
+        if (!entity.isExternal()) {
+            // Delegate entity registration orchestration to the registry
+            registry.registerEntity(entity, this)
+            
+            // Add dynamic methods to the class
+            addStaticMethods(entity)
+            addInstanceMethods(entity)
         }
     }
 
-    /**
-     * Obtain the qualifiers that need eagerly-created API providers.
-     *
-     * Entities expanded to every connection source or tenant still need datastore routing entries
-     * for those qualifiers, but the expensive API providers can be created lazily when a qualifier
-     * is actually used.
-     *
-     * @param entity The persistent entity
-     * @param qualifiers All datastore qualifiers for the entity
-     * @return The qualifiers to eagerly initialize API providers for
-     */
-    protected List<String> apiQualifiers(PersistentEntity entity, List<String> qualifiers) {
-        List<String> configuredQualifiers = new ArrayList<>(ConnectionSourcesSupport.getConnectionSourceNames(entity))
-        if (configuredQualifiers.contains(ConnectionSource.ALL)) {
-            return [ConnectionSource.DEFAULT]
-        }
-
-        boolean isMultiTenant = MultiTenant.isAssignableFrom(entity.javaClass)
-        if (isMultiTenant && configuredQualifiers.equals(ConnectionSourcesSupport.DEFAULT_CONNECTION_SOURCE_NAMES)) {
-            return [ConnectionSource.DEFAULT]
-        }
-
-        return qualifiers
-    }
-
     /**
      * Obtain all of the qualifiers (typically the connection names) for the datastore and entity
      *
@@ -212,18 +118,11 @@ class GormEnhancer implements Closeable {
      * @param entity The entity
      * @return The qualifiers
      */
+    @CompileDynamic
     List<String> allQualifiers(Datastore datastore, PersistentEntity entity) {
         List<String> qualifiers = new ArrayList<>()
         qualifiers.addAll(ConnectionSourcesSupport.getConnectionSourceNames(entity))
 
-        // For MultiTenant entities OR entities declared with ConnectionSource.ALL,
-        // expand qualifiers to include all available connection sources — BUT only
-        // if the entity does not have an explicit non-DEFAULT datasource declaration.
-        //
-        // When a MultiTenant entity declares `datasource 'secondary'`, that explicit
-        // mapping must be preserved. Expanding to all connections causes silent
-        // data routing to the wrong database (the DEFAULT datasource) for
-        // DISCRIMINATOR multi-tenancy mode.
         boolean isMultiTenant = MultiTenant.isAssignableFrom(entity.javaClass)
         boolean hasExplicitAll = qualifiers.contains(ConnectionSource.ALL)
         boolean hasExplicitNonDefaultDatasource = isMultiTenant &&
@@ -231,480 +130,154 @@ class GormEnhancer implements Closeable {
                 qualifiers.size() > 0 &&
                 !qualifiers.equals(ConnectionSourcesSupport.DEFAULT_CONNECTION_SOURCE_NAMES)
 
-        if ((isMultiTenant || hasExplicitAll) && !hasExplicitNonDefaultDatasource && (datastore instanceof ConnectionSourcesProvider)) {
+        if ((isMultiTenant || hasExplicitAll) && !hasExplicitNonDefaultDatasource) {
             qualifiers.clear()
-            qualifiers.add(ConnectionSource.DEFAULT)
-
-            Iterable<ConnectionSource> allConnectionSources = ((ConnectionSourcesProvider) datastore).getConnectionSources().allConnectionSources
-            Collection<String> allConnectionSourceNames = allConnectionSources.findAll() { ConnectionSource connectionSource -> connectionSource.name != ConnectionSource.DEFAULT }
-                                                                              .collect() { ((ConnectionSource) it).name }
-            qualifiers.addAll(allConnectionSourceNames)
-        }
-        return qualifiers
-    }
-
-    /**
-     * Find the tenant id for the given entity
-     *
-     * @param entity
-     * @return
-     */
-    protected static String findTenantId(Class entity) {
-        if (MultiTenant.isAssignableFrom(entity)) {
-            Datastore defaultDatastore = findDatastore(entity, ConnectionSource.DEFAULT)
-            if ((defaultDatastore instanceof MultiTenantCapableDatastore)) {
-
-                MultiTenantCapableDatastore multiTenantCapableDatastore = (MultiTenantCapableDatastore) defaultDatastore
-                if (multiTenantCapableDatastore.getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) {
-                    return Tenants.currentId(multiTenantCapableDatastore)
+            if (datastore == this.datastore) {
+                qualifiers.addAll(connectionSourceNames)
+            } else {
+                def className = entity.name
+                for (String q in connectionSourceNames) {
+                    if (registry.getDatastore(className, q) == datastore) {
+                        qualifiers.add(q)
+                    }
                 }
-                else {
-                    return ConnectionSource.DEFAULT
+
+                if (qualifiers.isEmpty()) {
+                    for (String q in registry.datastoresByQualifier.keySet()) {
+                        if (registry.datastoresByQualifier.get(q) == datastore) {
+                            qualifiers.add(q)
+                        }
+                    }
                 }
             }
-            else {
-                log.debug('Return default tenant id for non-multitenant capable datastore')
-                return ConnectionSource.DEFAULT
-            }
-        }
-        else {
-            log.debug('Returning default tenant id for non-multitenant class [{}]', entity)
-            return ConnectionSource.DEFAULT
         }
-    }
-
-    /**
-     * Find a static API for the give entity type and qualifier (the connection name)
-     *
-     * @param entity The entity class
-     * @param qualifier The qualifier
-     * @return A static API
-     *
-     * @throws IllegalStateException if no static API is found for the type
-     */
-    static <D> GormStaticApi<D> findStaticApi(Class<D> entity, String qualifier = findTenantId(entity)) {
-        String className = NameUtils.getClassName(entity)
-        def staticApi = STATIC_APIS.get(qualifier)?.get(className)
-        if (staticApi == null) {
-            staticApi = initializeStaticApi(entity, qualifier, className)
-        }
-        if (staticApi == null) {
-            throw stateException(entity)
-        }
-        return staticApi
-    }
-
-    private static <D> GormStaticApi<D> initializeStaticApi(Class<D> entity, String qualifier, String className) {
-        GormEnhancer enhancer = findEnhancer(entity, qualifier, className)
-        if (enhancer == null) {
-            return null
-        }
-        return (GormStaticApi<D>) STATIC_APIS.get(qualifier).computeIfAbsent(className) { String ignored ->
-            enhancer.getStaticApi(entity, qualifier)
-        }
-    }
-
-    /**
-     * Find an instance API for the give entity type and qualifier (the connection name)
-     *
-     * @param entity The entity class
-     * @param qualifier The qualifier
-     * @return An instance API
-     *
-     * @throws IllegalStateException if no instance API is found for the type
-     */
-    static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity, String qualifier = findTenantId(entity)) {
-        String className = NameUtils.getClassName(entity)
-        def instanceApi = INSTANCE_APIS.get(qualifier)?.get(className)
-        if (instanceApi == null) {
-            instanceApi = initializeInstanceApi(entity, qualifier, className)
-        }
-        if (instanceApi == null) {
-            throw stateException(entity)
-        }
-        return instanceApi
-    }
-
-    private static <D> GormInstanceApi<D> initializeInstanceApi(Class<D> entity, String qualifier, String className) {
-        GormEnhancer enhancer = findEnhancer(entity, qualifier, className)
-        if (enhancer == null) {
-            return null
-        }
-        return (GormInstanceApi<D>) INSTANCE_APIS.get(qualifier).computeIfAbsent(className) { String ignored ->
-            enhancer.getInstanceApi(entity, qualifier)
-        }
-    }
-
-    /**
-     * Find a validation API for the give entity type and qualifier (the connection name)
-     *
-     * @param entity The entity class
-     * @param qualifier The qualifier
-     * @return A validation API
-     *
-     * @throws IllegalStateException if no validation API is found for the type
-     */
-    static <D> GormValidationApi<D> findValidationApi(Class<D> entity, String qualifier = findTenantId(entity)) {
-        String className = NameUtils.getClassName(entity)
-        def validationApi = VALIDATION_APIS.get(qualifier)?.get(className)
-        if (validationApi == null) {
-            validationApi = initializeValidationApi(entity, qualifier, className)
-        }
-        if (validationApi == null) {
-            throw stateException(entity)
-        }
-        return validationApi
-    }
-
-    private static <D> GormValidationApi<D> initializeValidationApi(Class<D> entity, String qualifier, String className) {
-        GormEnhancer enhancer = findEnhancer(entity, qualifier, className)
-        if (enhancer == null) {
-            return null
-        }
-        return (GormValidationApi<D>) VALIDATION_APIS.get(qualifier).computeIfAbsent(className) { String ignored ->
-            enhancer.getValidationApi(entity, qualifier)
-        }
-    }
-
-    private static GormEnhancer findEnhancer(Class entity, String qualifier, String className) {
-        Datastore datastore = DATASTORES.get(qualifier)?.get(className)
-        datastore != null ? ENHANCERS.get(datastore) : null
-    }
-
-    /**
-     * Find a datastore for the give entity type and qualifier (the connection name)
-     *
-     * @param entity The entity class
-     * @param qualifier The qualifier
-     * @return A datastore
-     *
-     * @throws IllegalStateException if no datastore is found for the type
-     */
-    static Datastore findDatastore(Class entity, String qualifier = findTenantId(entity)) {
-        def datastore = DATASTORES.get(qualifier)?.get(entity.name)
-        if (datastore == null) {
-            throw stateException(entity)
-        }
-        return datastore
-    }
-
-    /**
-     * Finds a datastore by type
-     *
-     * @param datastoreType The datastore type
-     * @return The datastore
-     *
-     * @throws IllegalStateException If no datastore is found for the type
-     */
-    static Datastore findDatastoreByType(Class<? extends Datastore> datastoreType) {
-        Datastore datastore = DATASTORES_BY_TYPE.get(datastoreType)
-        if (datastore == null) {
-            throw new IllegalStateException("No GORM implementation configured for type [$datastoreType]. Ensure GORM has been initialized correctly")
-        }
-        return datastore
-    }
 
-    /**
-     * Finds a single datastore
-     *
-     * @throws IllegalStateException If no datastore is found or more than one is configured
-     */
-    static Datastore findSingleDatastore() {
-        Collection<Datastore> allDatastores = DATASTORES_BY_TYPE.values()
-        if (allDatastores.isEmpty()) {
-            throw new IllegalStateException('No GORM implementations configured. Ensure GORM has been initialized correctly')
-        }
-        else if (allDatastores.size() > 1) {
-            throw new IllegalStateException('More than one GORM implementation is configured. Specific the datastore type!')
-        }
-        else {
-            return allDatastores.first()
+        if (qualifiers.isEmpty()) {
+            qualifiers.add(ConnectionSource.DEFAULT)
         }
+        return qualifiers.unique()
     }
 
-    /**
-     * Finds a single available transaction manager
-     *
-     * @return The transaction manager
-     *
-     * @throws TransactionSystemException If the current implementation does not support transactions
-     * @throws IllegalStateException If no GORM implementation has been bootstrapped and configured
-     */
-    static PlatformTransactionManager findSingleTransactionManager(String connectionName = ConnectionSource.DEFAULT) {
-        Datastore datastore = findSingleDatastore()
-        return getTransactionManagerForConnection(datastore, connectionName)
+    List<String> getConnectionSourceNames() {
+        return connectionSourceNames
     }
 
     /**
-     * Finds a single available transaction manager
-     *
-     * @return The transaction manager
-     *
-     * @throws TransactionSystemException If the current implementation does not support transactions
-     * @throws IllegalStateException If no GORM implementation has been bootstrapped and configured
+     * @return The GORM registry instance
      */
-    static PlatformTransactionManager findTransactionManager(Class<? extends Datastore> datastoreType, String connectionName = ConnectionSource.DEFAULT) {
-        Datastore datastore = findDatastoreByType(datastoreType)
-        return getTransactionManagerForConnection(datastore, connectionName)
+    static GormRegistry getRegistry() {
+        return GormRegistry.instance
     }
 
-    /**
-     * Find the entity for the given type
-     *
-     * @param entity The entity class
-     * @param qualifier The qualifier
-     * @return A entity
-     *
-     * @throws IllegalStateException if no entity is found for the type
-     */
-    static PersistentEntity findEntity(Class entity, String qualifier = findTenantId(entity)) {
-        findDatastore(entity, qualifier).getMappingContext().getPersistentEntity(entity.name)
+    static PersistentEntity findEntity(Class entity) {
+        GormApiResolver resolver = GormRegistry.instance.apiResolver
+        Datastore ds = resolver.findDatastore(entity, null)
+        return ds?.mappingContext?.getPersistentEntity(entity.name)
     }
 
     /**
      * Closes the enhancer clearing any stored static state
-     *
-     * @throws IOException
      */
-    @Override
     @CompileStatic
     void close() throws IOException {
-        if (!ENHANCERS.remove(datastore, this)) {
-            return
-        }
         removeConstraints()
-        DATASTORES_BY_TYPE.clear()
-        def registry = GroovySystem.metaClassRegistry
+        if (STATE_REGISTRY.getPreferredDatastore() == datastore) {
+            STATE_REGISTRY.clearPreferredDatastore()
+        }
+        registry.removeDatastore(datastore)
+        def metaClassRegistry = GroovySystem.metaClassRegistry
         for (entity in datastore.mappingContext.persistentEntities) {
-
-            List<String> qualifiers = allQualifiers(datastore, entity)
             def cls = entity.javaClass
             def className = cls.name
-            for (q in qualifiers) {
-                NAMED_QUERIES.remove(className)
-                if (STATIC_APIS.containsKey(q)) {
-                    STATIC_APIS.get(q).remove(className)
-                }
-                if (INSTANCE_APIS.containsKey(q)) {
-                    INSTANCE_APIS.get(q).remove(className)
-                }
-                if (VALIDATION_APIS.containsKey(q)) {
-                    VALIDATION_APIS.get(q).remove(className)
-                }
-                if (DATASTORES.containsKey(q)) {
-                    DATASTORES.get(q).remove(className)
-                }
+            registry.removeEntityDatastore(className, datastore)
+            
+            boolean stillManaged = (registry.getStaticApi(className) != null)
+            
+            if (!stillManaged) {
+                metaClassRegistry.removeMetaClass(cls)
             }
-            registry.removeMetaClass(cls)
         }
     }
 
-    private static PlatformTransactionManager getTransactionManagerForConnection(Datastore datastore, String connectionName) {
-        if (datastore instanceof TransactionCapableDatastore && ConnectionSource.DEFAULT.equals(connectionName)) {
-            return ((TransactionCapableDatastore) datastore).getTransactionManager()
-        } else if (datastore instanceof MultipleConnectionSourceCapableDatastore) {
-            Datastore datastoreForConnection = ((MultipleConnectionSourceCapableDatastore) datastore).getDatastoreForConnection(connectionName)
-            if (datastoreForConnection instanceof TransactionCapableDatastore) {
-                return ((TransactionCapableDatastore) datastoreForConnection).getTransactionManager()
-            }
-        }
-        throw new TransactionSystemException("Datastore implementation ${datastore.getClass().getName()} does not support transactions!")
-    }
-
-    private static IllegalStateException stateException(Class entity) {
-        new IllegalStateException("Either class [$entity.name] is not a domain class or GORM has not been initialized correctly or has already been shutdown. Ensure GORM is loaded and configured correctly before calling any methods on a GORM entity.")
-    }
-
     @CompileDynamic
     protected void removeConstraints() {
         try {
-            String className = 'org.apache.groovy.grails.validation.ConstrainedProperty'
-            ClassLoader classLoader = getClass().getClassLoader()
-            if (ClassUtils.isPresent(className, classLoader)) {
-                classLoader.loadClass(className).removeConstraint('unique')
-            }
-        } catch (Throwable e) {
-            log.debug("Not running in Grails 2 environment, cannot de-register constraints. This exception can be safely ignored if you are not using Grails 2. ${e.message}", e)
-        }
-    }
-
-    protected void registerConstraints(Datastore datastore) {
-        try {
-            String className = 'org.grails.datastore.gorm.support.ConstraintRegistrar'
-            ClassLoader classLoader = getClass().getClassLoader()
-            if (ClassUtils.isPresent(className, classLoader)) {
-                classLoader.loadClass(className).newInstance(datastore)
+            def cls = Class.forName('org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator', false, GormEnhancer.classLoader)
+            if (cls != null) {
+                def factory = datastore.mappingContext.mappingFactory
+                if (factory.hasProperty('entityContext')) {
+                    def constraintsEvaluator = factory.entityContext.getBean(cls)
+                    if (constraintsEvaluator != null) {
+                        for (entity in datastore.mappingContext.persistentEntities) {
+                            constraintsEvaluator.removeConstraints(entity.javaClass)
+                        }
+                    }
+                }
             }
         } catch (Throwable e) {
-            log.debug("Unable to register GORM constraints. Not running a Grails environment. This can be safely ignored if you are not running Grails: $e.message", e)
-        }
-    }
-
-    @CompileStatic
-    List<FinderMethod> getFinders() {
-        if (finders == null) {
-            finders = Collections.unmodifiableList(createDynamicFinders())
+            log.debug("Not running in Grails environment, cannot de-register constraints. ${e.message}")
         }
-        finders
     }
 
-    /**
-     * Enhances all persistent entities.
-     *
-     * @param onlyExtendedMethods If only to add additional methods provides by subclasses of the GORM APIs
-     */
-    @CompileStatic
-    void enhance(boolean onlyExtendedMethods = false) {
-        if (dynamicEnhance) {
-            for (PersistentEntity e in datastore.mappingContext.persistentEntities) {
-                if (e.external && !includeExternal) continue
-                enhance(e, onlyExtendedMethods)
-            }
-        }
-    }
-
-    /**
-     * Enhance and individual entity
-     *
-     * @param e The entity
-     * @param onlyExtendedMethods If only to add additional methods provides by subclasses of the GORM APIs
-     */
-    @CompileStatic
-    void enhance(PersistentEntity e, boolean onlyExtendedMethods = false) {
-        registerEntity(e)
-
-        if (!(GroovyObject.isAssignableFrom(e.javaClass)) || dynamicEnhance) {
-            addInstanceMethods(e, onlyExtendedMethods)
-
-            addStaticMethods(e, onlyExtendedMethods)
-        }
-    }
-
-    @CompileStatic
-    protected void addStaticMethods(PersistentEntity e, boolean onlyExtendedMethods) {
+    @CompileDynamic
+    protected void addStaticMethods(PersistentEntity e) {
         def cls = e.javaClass
         ExpandoMetaClass mc = MetaClassUtils.getExpandoMetaClass(cls)
-        def staticApiProvider = getStaticApi(cls)
-        for (Method m in (onlyExtendedMethods ? staticApiProvider.extendedMethods : staticApiProvider.methods)) {
-            def method = m
-            if (method != null) {
-                def methodName = method.name
-                def parameterTypes = method.parameterTypes
-                if (parameterTypes != null) {
-                    boolean realMethodExists = doesRealMethodExist(mc, methodName, parameterTypes, true)
-                    if (!realMethodExists) {
-                        registerStaticMethod(mc, methodName, parameterTypes, staticApiProvider)
-                    }
+        
+        mc.static.methodMissing = { String name, args ->
+            def api = registry.findStaticApi(cls, null)
+            try {
+                return api.invokeMethod(name, args)
+            } catch (MissingMethodException mme) {
+                if (mme.method == name && mme.type == api.class) {
+                    return api.methodMissing(name, args)
                 }
+                throw mme
+            }
+        }
+        mc.static.propertyMissing = { String name ->
+            def api = registry.findStaticApi(cls, null)
+            try {
+                return api.getProperty(name)
+            } catch (MissingPropertyException mpe) {
+                if (mpe.property == name && mpe.type == api.class) {
+                    return api.propertyMissing(name)
+                }
+                throw mpe
             }
         }
     }
 
     @CompileDynamic
-    protected void registerStaticMethod(ExpandoMetaClass mc, String methodName, Class<?>[] parameterTypes, GormStaticApi staticApiProvider) {
-        def callable = new StaticMethodInvokingClosure(staticApiProvider, methodName, parameterTypes)
-        mc.static."$methodName" = callable
-    }
-
-    protected boolean appliesToDatastore(Datastore datastore, PersistentEntity entity) {
-        !entity.isExternal()
-    }
-
-    @CompileDynamic
-    protected <D> List<AbstractGormApi<D>> getInstanceMethodApiProviders(Class<D> cls) {
-        [getInstanceApi(cls), getValidationApi(cls)]
-    }
-
-    @CompileStatic
-    protected void addInstanceMethods(PersistentEntity e, boolean onlyExtendedMethods) {
+    protected void addInstanceMethods(PersistentEntity e) {
         Class cls = e.javaClass
         ExpandoMetaClass mc = MetaClassUtils.getExpandoMetaClass(cls)
-        for (AbstractGormApi apiProvider in getInstanceMethodApiProviders(cls)) {
-
-            for (Method method in (onlyExtendedMethods ? apiProvider.extendedMethods : apiProvider.methods)) {
-                def methodName = method.name
-                Class[] parameterTypes = method.parameterTypes
-
-                if (parameterTypes) {
-                    parameterTypes = (parameterTypes.length == 1 ? [] : parameterTypes[1..-1]) as Class[]
-
-                    boolean realMethodExists = doesRealMethodExist(mc, methodName, parameterTypes, false)
-
-                    if (!realMethodExists) {
-                        registerInstanceMethod(cls, mc, apiProvider, methodName, parameterTypes)
-                    }
+        
+        mc.methodMissing = { String name, args ->
+            def api = registry.findInstanceApi(cls, null)
+            try {
+                return api.invokeMethod(name, args)
+            } catch (MissingMethodException mme) {
+                if (mme.method == name && mme.type == api.class) {
+                    return api.methodMissing(delegate, name, args)
                 }
+                throw mme
             }
         }
-    }
-
-    protected registerInstanceMethod(Class cls, ExpandoMetaClass mc, AbstractGormApi apiProvider, String methodName, Class[] parameterTypes) {
-        // use fake object just so we have the right method signature
-        final tooCall = new InstanceMethodInvokingClosure(apiProvider, cls, methodName, parameterTypes)
-        def pt = parameterTypes
-        // Hack to workaround http://jira.codehaus.org/browse/GROOVY-4720
-        final closureMethod = new ClosureStaticMetaMethod(methodName, cls, tooCall, pt) {
-            @Override
-            int getModifiers() { Modifier.PUBLIC }
-        }
-        mc.registerInstanceMethod(closureMethod)
-    }
-
-    @CompileStatic
-    protected static boolean doesRealMethodExist(final MetaClass mc, final String methodName, final Class[] parameterTypes, boolean staticScope) {
-        boolean realMethodExists = false
-        try {
-            MetaMethod existingMethod = mc.pickMethod(methodName, parameterTypes)
-            if (existingMethod && existingMethod.isStatic() == staticScope && isRealMethod(existingMethod) && parameterTypes.length == existingMethod.parameterTypes.length)  {
-                realMethodExists = true
-            }
-        } catch (MethodSelectionException mse) {
-            // the metamethod already exists with multiple signatures, must check if the exact method exists
-            realMethodExists = mc.methods.contains { MetaMethod existingMethod ->
-                existingMethod.name == methodName && existingMethod.isStatic() == staticScope && isRealMethod(existingMethod) && ((!parameterTypes && !existingMethod.parameterTypes) || parameterTypes == existingMethod.parameterTypes)
+        mc.propertyMissing = { String name ->
+            def api = registry.findInstanceApi(cls, null)
+            try {
+                return api.getProperty(name)
+            } catch (MissingPropertyException mpe) {
+                if (mpe.property == name && mpe.type == api.class) {
+                    return api.propertyMissing(delegate, name)
+                }
+                throw mpe
             }
         }
-        return realMethodExists
-    }
-
-    @CompileStatic
-    protected static boolean isRealMethod(MetaMethod existingMethod) {
-        existingMethod instanceof CachedMethod
-    }
-
-    @CompileStatic
-    protected <D> GormStaticApi<D> getStaticApi(Class<D> cls, String qualifier = ConnectionSource.DEFAULT) {
-        new GormStaticApi<D>(cls, datastore, getFinders(), transactionManager)
-    }
-
-    @CompileStatic
-    protected <D> GormInstanceApi<D> getInstanceApi(Class<D> cls, String qualifier = ConnectionSource.DEFAULT) {
-        def instanceApi = new GormInstanceApi<D>(cls, datastore)
-        instanceApi.failOnError = failOnError
-        instanceApi.markDirty = markDirty
-        return instanceApi
-    }
-
-    @CompileStatic
-    protected <D> GormValidationApi<D> getValidationApi(Class<D> cls, String qualifier = ConnectionSource.DEFAULT) {
-        new GormValidationApi(cls, datastore)
-    }
-
-    @CompileStatic
-    protected List<FinderMethod> createDynamicFinders() {
-        Datastore targetDatastore = datastore
-        createDynamicFinders(targetDatastore)
+        mc.propertyMissing = { String name, val ->
+            registry.findInstanceApi(cls, null).setProperty(name, val)
+        }
     }
 
-    @CompileStatic
-    protected List<FinderMethod> createDynamicFinders(Datastore targetDatastore) {
-        [new FindOrCreateByFinder(targetDatastore),
-         new FindOrSaveByFinder(targetDatastore),
-         new FindByFinder(targetDatastore),
-         new FindAllByFinder(targetDatastore),
-         new FindAllByBooleanFinder(targetDatastore),
-         new FindByBooleanFinder(targetDatastore),
-         new CountByFinder(targetDatastore),
-         new ListOrderByFinder(targetDatastore)] as List<FinderMethod>
-    }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancerRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancerRegistry.groovy
new file mode 100644
index 0000000000..e79a0f5496
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancerRegistry.groovy
@@ -0,0 +1,99 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.mapping.core.Datastore
+
+/**
+ * Singleton registry for managing GormEnhancer's static state.
+ * 
+ * This class holds thread-local and shared state that was previously
+ * defined as static fields in GormEnhancer, allowing for better isolation
+ * and testability.
+ *
+ * @author Graeme Rocher
+ */
+@CompileStatic
+class GormEnhancerRegistry {
+
+    private static final GormEnhancerRegistry INSTANCE = new GormEnhancerRegistry()
+
+    private final ThreadLocal<Integer> resolvingDatastore = ThreadLocal.withInitial { 0 }
+    private final ThreadLocal<Datastore> preferredDatastore = new ThreadLocal<>()
+
+    /**
+     * @return The singleton instance
+     */
+    static GormEnhancerRegistry getInstance() {
+        return INSTANCE
+    }
+
+    /**
+     * Set the resolving datastore depth for the current thread
+     *
+     * @param depth The depth
+     */
+    void setResolvingDatastoreDepth(int depth) {
+        resolvingDatastore.set(depth)
+    }
+
+    /**
+     * Get the resolving datastore depth for the current thread
+     *
+     * @return The depth
+     */
+    int getResolvingDatastoreDepth() {
+        return resolvingDatastore.get()
+    }
+
+    /**
+     * Clear the resolving datastore depth for the current thread
+     */
+    void clearResolvingDatastoreDepth() {
+        resolvingDatastore.remove()
+    }
+
+    /**
+     * Set the preferred datastore for the current thread
+     *
+     * @param datastore The datastore
+     */
+    void setPreferredDatastore(Datastore datastore) {
+        preferredDatastore.set(datastore)
+    }
+
+    /**
+     * Get the preferred datastore for the current thread
+     *
+     * @return The datastore, or null if none is set
+     */
+    Datastore getPreferredDatastore() {
+        return preferredDatastore.get()
+    }
+
+    /**
+     * Clear the preferred datastore for the current thread
+     */
+    void clearPreferredDatastore() {
+        preferredDatastore.remove()
+    }
+
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntity.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntity.groovy
index dae59760d8..6e3d8c380a 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntity.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntity.groovy
@@ -61,7 +61,7 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
     @Generated
     @CompileDynamic
     def propertyMissing(String name) {
-        GormEnhancer.findInstanceApi(getClass()).propertyMissing(this, name)
+        GormRegistry.instance.findInstanceApi(getClass()).propertyMissing(this, name)
     }
 
     /**
@@ -453,7 +453,7 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
      */
     @Generated
     static PersistentEntity getGormPersistentEntity() {
-        currentGormStaticApi().persistentEntity
+        currentGormStaticApi().getGormPersistentEntity()
     }
 
     @Generated
@@ -544,6 +544,25 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
         currentGormStaticApi().saveAll(objectsToSave)
     }
 
+    /**
+     * Deletes all objects
+     * @return The number of objects deleted
+     */
+    @Generated
+    static Number deleteAll() {
+        currentGormStaticApi().deleteAll()
+    }
+
+    /**
+     * Deletes all objects for the given arguments
+     * @param params The arguments
+     * @return The number of objects deleted
+     */
+    @Generated
+    static Number deleteAll(Map params) {
+        currentGormStaticApi().deleteAll(params)
+    }
+
     /**
      * Deletes a list of objects in one go
      * @param objectsToDelete The objects to delete
@@ -855,9 +874,15 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
     @Generated
     static Object staticPropertyMissing(String property) {
         try {
-            currentGormStaticApi().propertyMissing(property)
-        } catch (IllegalStateException e) {
-            throw new MissingPropertyException(property, this)
+            def result = currentGormStaticApi().propertyMissing(property)
+            if (result == null) {
+                throw new MissingPropertyException(property, this)
+            }
+            return result
+        } catch (MissingPropertyException e) {
+            throw e
+        } catch (Throwable e) {
+            throw new MissingPropertyException(property, this, e)
         }
     }
 
@@ -1450,12 +1475,22 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
     }
 
     @Generated
-    private GormInstanceApi<D> currentGormInstanceApi() {
-        (GormInstanceApi<D>) GormEnhancer.findInstanceApi(getClass())
+    GormInstanceApi<D> currentGormInstanceApi() {
+        Class<D> cls = (Class<D>) getClass()
+        GormInstanceApi<D> api = GormRegistry.instance.resolveInstanceApi(cls)
+        if (api == null) {
+            throw new IllegalStateException("No GORM implementation configured for class [${cls.name}]. Ensure GORM has been initialized correctly")
+        }
+        api
     }
 
     @Generated
-    private static GormStaticApi<D> currentGormStaticApi() {
-        (GormStaticApi<D>) GormEnhancer.findStaticApi(this)
+    static GormStaticApi<D> currentGormStaticApi() {
+        Class<D> cls = (Class<D>) this
+        GormStaticApi<D> api = GormRegistry.instance.resolveStaticApi(cls)
+        if (api == null) {
+            throw new IllegalStateException("No GORM implementation configured for class [${cls.name}]. Ensure GORM has been initialized correctly")
+        }
+        api
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntityDirtyCheckable.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntityDirtyCheckable.groovy
index 8d09901092..998ed5dc2e 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntityDirtyCheckable.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEntityDirtyCheckable.groovy
@@ -39,7 +39,7 @@ trait GormEntityDirtyCheckable extends DirtyCheckable {
     @Override
     @Generated
     boolean hasChanged(String propertyName) {
-        PersistentEntity entity = currentGormInstanceApi().persistentEntity
+        PersistentEntity entity = currentGormInstanceApi().getGormPersistentEntity()
 
         PersistentProperty persistentProperty = entity.getPropertyByName(propertyName)
         if (!persistentProperty) {
@@ -58,6 +58,10 @@ trait GormEntityDirtyCheckable extends DirtyCheckable {
 
     @Generated
     private GormInstanceApi currentGormInstanceApi() {
-        (GormInstanceApi) GormEnhancer.findInstanceApi(getClass())
+        GormInstanceApi api = (GormInstanceApi) GormRegistry.instance.findInstanceApi(getClass())
+        if (api == null) {
+            throw new IllegalStateException("No GORM implementation configured for class [${getClass().name}]. Ensure GORM has been initialized correctly")
+        }
+        api
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
index 24f2bbeb6a..d7a5217e77 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
@@ -4,79 +4,128 @@
  *  distributed with this work for additional information
  *  regarding copyright ownership.  The ASF licenses this file
  *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
+ *  'License'); you may not use this file except in compliance
  *  with the License.  You may obtain a copy of the License at
  *
  *    https://www.apache.org/licenses/LICENSE-2.0
  *
  *  Unless required by applicable law or agreed to in writing,
  *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  *  KIND, either express or implied.  See the License for the
  *  specific language governing permissions and limitations
  *  under the License.
  */
 package org.grails.datastore.gorm
 
-import groovy.transform.CompileStatic
+import groovy.transform.CompileDynamic
 import org.codehaus.groovy.runtime.InvokerHelper
 
+import org.springframework.transaction.PlatformTransactionManager
+
 import grails.gorm.api.GormInstanceOperations
+import org.grails.datastore.gorm.schemaless.DynamicAttributes
 import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.datastore.mapping.core.Session
 import org.grails.datastore.mapping.core.SessionCallback
-import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.VoidSessionCallback
 import org.grails.datastore.mapping.core.connections.ConnectionSources
 import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
 import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
-import org.grails.datastore.mapping.dirty.checking.DirtyCheckingSupport
-import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.proxy.EntityProxy
-import org.grails.datastore.mapping.reflect.EntityReflector
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 import org.grails.datastore.mapping.validation.ValidationException
 
 /**
- * Instance methods of the GORM API.
+ * GORM instance API implementation.
  *
  * @author Graeme Rocher
- * @param <D> the entity/domain class
+ * @since 1.0
  */
-@CompileStatic
+@CompileDynamic
 class GormInstanceApi<D> extends AbstractGormApi<D> implements GormInstanceOperations<D> {
 
-    Class<? extends Exception> validationException = ValidationException
+    Class<? extends Exception> validationException = ValidationException.VALIDATION_EXCEPTION_TYPE
     boolean failOnError = false
     boolean markDirty = true
+    protected final GormRegistry registry
 
     GormInstanceApi(Class<D> persistentClass, Datastore datastore) {
+        this(persistentClass, datastore, null)
+    }
+
+    GormInstanceApi(Class<D> persistentClass, Datastore datastore, GormRegistry registry) {
         super(persistentClass, datastore)
-        validationException = ValidationException.VALIDATION_EXCEPTION_TYPE
+        this.registry = registry ?: GormEnhancer.getRegistry()
+        this.failOnError = false
+        this.markDirty = true
     }
 
-    Object propertyMissing(D instance, String name) {
-        try {
+    GormInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver) {
+        this(persistentClass, mappingContext, datastoreResolver, null)
+    }
+
+    GormInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver, GormRegistry registry) {
+        super(persistentClass, mappingContext, datastoreResolver)
+        this.registry = registry ?: GormEnhancer.getRegistry()
+        this.failOnError = false
+        this.markDirty = true
+    }
 
-            def instanceApi = GormEnhancer.findInstanceApi(persistentClass, name)
-            return new DelegatingGormEntityApi(instanceApi, instance)
-        } catch (IllegalStateException ise) {
-            throw new MissingPropertyException(name, persistentClass)
+    @Override
+    PlatformTransactionManager getTransactionManager() {
+        Datastore ds = getDatastore()
+        if (ds instanceof TransactionCapableDatastore) {
+            return ((TransactionCapableDatastore) ds).getTransactionManager()
         }
+        return null
     }
 
-    /**
-     * Proxy aware instanceOf implementation.
-     */
-    boolean instanceOf(D o, Class cls) {
-        if (o instanceof EntityProxy) {
-            o = (D) ((EntityProxy)o).getTarget()
+    @Override
+    protected <T1> T1 executeQualified(String qualifier, SessionCallback<T1> callback) {
+        GormInstanceApi<D> qualifiedApi = registry.findInstanceApi(persistentClass, qualifier)
+        if (qualifiedApi != null && qualifiedApi != this) {
+            return (T1) qualifiedApi.execute(callback)
         }
-        return o in cls
+        return DatastoreUtils.execute(getDatastore(), callback)
     }
 
-    /**
-     * Upgrades an existing persistence instance to a write lock
-     * @return The instance
-     */
+    GormInstanceApi<D> forQualifier(String qualifier) {
+        DatastoreResolver resolver = registry.createClassDatastoreResolver(persistentClass, qualifier)
+        GormInstanceApi<D> newApi = new GormInstanceApi<D>(persistentClass, mappingContext, resolver, registry)
+        newApi.failOnError = failOnError
+        newApi.markDirty = markDirty
+        return newApi
+    }
+
+    @Override
+    Object propertyMissing(D instance, String name) {
+        Datastore ds = getDatastore()
+        if (ds instanceof ConnectionSourcesProvider) {
+            ConnectionSources sources = ((ConnectionSourcesProvider) ds).connectionSources
+            if (sources != null && sources.getConnectionSource(name) != null) {
+                def instanceApi = registry.resolveInstanceApi(persistentClass, name)
+                return new DelegatingGormEntityApi(instanceApi, instance)
+            }
+        }
+        if (instance instanceof DynamicAttributes) {
+            return ((DynamicAttributes) instance).getAt(name)
+        }
+        throw new MissingPropertyException(name, persistentClass)
+    }
+
+    @Override
+    boolean instanceOf(D instance, Class cls) {
+        if (instance == null) return false
+        if (instance instanceof EntityProxy) {
+            return cls.isInstance(((EntityProxy) instance).getTarget())
+        }
+        return cls.isInstance(instance)
+    }
+
+    @Override
     D lock(D instance) {
         execute({ Session session ->
             session.lock(instance)
@@ -84,29 +133,15 @@ class GormInstanceApi<D> extends AbstractGormApi<D> implements GormInstanceOpera
         } as SessionCallback)
     }
 
-    /**
-     * Locks the instance for updates for the scope of the passed closure
-     *
-     * @param callable The closure
-     * @return The result of the closure
-     */
-    <T> T mutex(D instance, Closure<T> callable) {
+    @Override
+    def <T> T mutex(D instance, Closure<T> callable) {
         execute({ Session session ->
-            try {
-                session.lock(instance)
-                callable?.call()
-            }
-            finally {
-                session.unlock(instance)
-            }
+            session.lock(instance)
+            callable?.call()
         } as SessionCallback)
     }
 
-    /**
-     * Refreshes the state of the current instance
-     * @param instance The instance
-     * @return The instance
-     */
+    @Override
     D refresh(D instance) {
         execute({ Session session ->
             session.refresh(instance)
@@ -115,262 +150,165 @@ class GormInstanceApi<D> extends AbstractGormApi<D> implements GormInstanceOpera
     }
 
     /**
-     * Saves an object the datastore
-     * @param instance The instance
-     * @return Returns the instance
+     * Implementation of read() for GormInstanceApi
      */
-    D save(D instance) {
-        save(instance, Collections.emptyMap())
+    D read(Serializable id) {
+        execute({ org.grails.datastore.mapping.core.Session session ->
+            session.retrieve(persistentClass, id)
+        } as org.grails.datastore.mapping.core.SessionCallback) as D
     }
 
-    /**
-     * Forces an insert of an object to the datastore
-     * @param instance The instance
-     * @return Returns the instance
-     */
-    D insert(D instance) {
-        insert(instance, Collections.emptyMap())
+    @Override
+    D merge(D instance, Map args) {
+        save(instance, args)
     }
 
-    /**
-     * Forces an insert of an object to the datastore
-     * @param instance The instance
-     * @return Returns the instance
-     */
-    D insert(D instance, Map params) {
-        execute({ Session session ->
-            doSave(instance, params, session, true)
-        } as SessionCallback)
-    }
-
-    /**
-     * Saves an object the datastore
-     * @param instance The instance
-     * @return Returns the instance
-     */
+    @Override
     D merge(D instance) {
-        save(instance, Collections.emptyMap())
+        save(instance, [:])
     }
 
-    /**
-     * Saves an object the datastore
-     * @param instance The instance
-     * @return Returns the instance
-     */
-    D merge(D instance, Map params) {
-        save(instance, params)
+    @Override
+    D save(D instance) {
+        save(instance, [:])
     }
 
-    /**
-     * Save method that takes a boolean which indicates whether to perform validation or not
-     *
-     * @param instance The instance
-     * @param validate Whether to perform validation
-     *
-     * @return The instance or null if validation fails
-     */
+    @Override
     D save(D instance, boolean validate) {
         save(instance, [validate: validate])
     }
 
-    /**
-     * Saves an object with the given parameters
-     * @param instance The instance
-     * @param params The parameters
-     * @return The instance
-     */
-    D save(D instance, Map params) {
+    @Override
+    D save(D instance, Map arguments) {
+        boolean shouldFlush = arguments?.containsKey('flush') ? (boolean)arguments.get('flush') : false
+        boolean validate = arguments?.containsKey('validate') ? (boolean)arguments.get('validate') : true
+        boolean previousSkipValidation = false
+        boolean restoreSkipValidation = false
+
+        if (validate) {
+            if (!registry.resolveValidationApi(persistentClass, qualifier).validate(instance, arguments)) {
+                if (shouldFail(arguments)) {
+                    throw validationException.newInstance('Validation Error(s) occurred during save()', instance.errors)
+                }
+                return null
+            }
+        } else {
+            registry.resolveValidationApi(persistentClass, qualifier).clearErrors(instance)
+            if (instance instanceof GormValidateable) {
+                GormValidateable gormValidateable = (GormValidateable) instance
+                previousSkipValidation = gormValidateable.shouldSkipValidation()
+                gormValidateable.skipValidation(true)
+                restoreSkipValidation = true
+            }
+        }
+
+        try {
+            execute({ Session session ->
+                session.persist(instance)
+                if (shouldFlush) {
+                    session.flush()
+                }
+                return instance
+            } as SessionCallback)
+        } finally {
+            if (restoreSkipValidation) {
+                ((GormValidateable) instance).skipValidation(previousSkipValidation)
+            }
+        }
+    }
+
+    private boolean shouldFail(Map arguments) {
+        if (arguments?.containsKey('failOnError')) {
+            return (boolean)arguments.get('failOnError')
+        }
+        return failOnError
+    }
+
+    @Override
+    D insert(D instance) {
+        insert(instance, [:])
+    }
+
+    @Override
+    D insert(D instance, Map arguments) {
+        boolean shouldFlush = arguments?.containsKey('flush') ? (boolean)arguments.get('flush') : false
         execute({ Session session ->
-            doSave(instance, params, session)
+            session.insert(instance)
+            if (shouldFlush) {
+                session.flush()
+            }
+            return instance
         } as SessionCallback)
     }
 
-    /**
-     * Returns the objects identifier
-     */
-    Serializable ident(D instance) {
-        PersistentProperty identity = persistentEntity.getIdentity()
-        if (identity != null) {
-            return (Serializable) instance[identity.name]
-        }
-        else {
-            PersistentProperty[] idProperties = persistentEntity.getCompositeIdentity()
-            if (idProperties != null) {
-                EntityReflector entityReflector = persistentEntity.getReflector()
-                def idInstance = persistentEntity.newInstance()
-                if (idInstance instanceof Serializable) {
-                    for (prop in idProperties) {
-                        String propertName = prop.name
-                        entityReflector.setProperty(
-                                idInstance, propertName, entityReflector.getProperty(instance, propertName)
-                        )
-                    }
-                    return (Serializable) idInstance
-                }
+    @Override
+    void delete(D instance) {
+        delete(instance, [:])
+    }
+
+    @Override
+    void delete(D instance, Map arguments) {
+        boolean shouldFlush = arguments?.containsKey('flush') ? (boolean)arguments.get('flush') : false
+        execute({ Session session ->
+            session.delete(instance)
+            if (shouldFlush) {
+                session.flush()
             }
-        }
-        return null
+        } as VoidSessionCallback)
     }
 
-    /**
-     * Attaches an instance to an existing session. Requries a session-based model
-     * @param instance The instance
-     * @return
-     */
+    @Override
+    Serializable ident(D instance) {
+        (Serializable)InvokerHelper.getProperty(instance, 'id')
+    }
+
+    @Override
     D attach(D instance) {
         execute({ Session session ->
             session.attach(instance)
-            instance
+            return instance
         } as SessionCallback)
     }
 
-    /**
-     * No concept of session-based model so defaults to true
-     */
+    @Override
     boolean isAttached(D instance) {
         execute({ Session session ->
             session.contains(instance)
         } as SessionCallback)
     }
 
-    /**
-     * Discards any pending changes. Requires a session-based model.
-     */
+    @Override
     void discard(D instance) {
         execute({ Session session ->
             session.clear(instance)
         } as SessionCallback)
     }
 
-    /**
-     * Deletes an instance from the datastore
-     * @param instance The instance to delete
-     */
-    void delete(D instance) {
-        delete(instance, Collections.emptyMap())
-    }
-
-    /**
-     * Deletes an instance from the datastore
-     * @param instance The instance to delete
-     */
-    void delete(D instance, Map params) {
-        execute({ Session session ->
-            session.delete(instance)
-            if (params?.flush) {
-                session.flush()
-            }
-        } as SessionCallback)
-    }
-
-    /**
-     * Checks whether a field is dirty
-     *
-     * @param instance The instance
-     * @param fieldName The name of the field
-     *
-     * @return true if the field is dirty
-     */
-    boolean isDirty(D instance, String fieldName) {
+    boolean isDirty(D instance) {
         if (instance instanceof DirtyCheckable) {
-            return ((DirtyCheckable) instance).hasChanged(fieldName)
+            return ((DirtyCheckable)instance).hasChanged()
         }
-        return true
+        return false
     }
 
-    /**
-     * Checks whether an entity is dirty
-     *
-     * @param instance The instance
-     * @return true if it is dirty
-     */
-    boolean isDirty(D instance) {
+    boolean isDirty(D instance, String fieldName) {
         if (instance instanceof DirtyCheckable) {
-            return ((DirtyCheckable) instance).hasChanged() || DirtyCheckingSupport.areAssociationsDirty(persistentEntity, instance)
+            return ((DirtyCheckable)instance).hasChanged(fieldName)
         }
-        return true
+        return false
     }
 
-    /**
-     * Obtains a list of property names that are dirty
-     *
-     * @param instance The instance
-     * @return A list of property names that are dirty
-     */
-    List getDirtyPropertyNames(D instance) {
+    List<String> getDirtyPropertyNames(D instance) {
         if (instance instanceof DirtyCheckable) {
-            return ((DirtyCheckable) instance).listDirtyPropertyNames()
+            return ((DirtyCheckable)instance).listDirtyPropertyNames()
         }
-        return []
+        return Collections.emptyList()
     }
 
-    /**
-     * Gets the original persisted value of a field.
-     *
-     * @param fieldName The field name
-     * @return The original persisted value
-     */
     Object getPersistentValue(D instance, String fieldName) {
         if (instance instanceof DirtyCheckable) {
-            return ((DirtyCheckable) instance).getOriginalValue(fieldName)
+            return ((DirtyCheckable)instance).getOriginalValue(fieldName)
         }
         return null
     }
-
-    protected D doSave(D instance, Map params, Session session, boolean isInsert = false) {
-        boolean hasErrors = false
-        boolean validate = params?.containsKey('validate') ? params.validate : true
-        boolean shouldFlush = params?.flush ? params.flush : false
-        if (instance instanceof GormValidateable) {
-
-            def validateable = (GormValidateable) instance
-            if (validate) {
-                validateable.skipValidation(false)
-                if (datastore instanceof ConnectionSourcesProvider) {
-                    ConnectionSources connectionSources = ((ConnectionSourcesProvider) datastore).connectionSources
-                    String connectionSourceName = connectionSources.defaultConnectionSource.name
-                    if (connectionSourceName != ConnectionSource.DEFAULT) {
-                        GormValidationApi<D> validationApi = GormEnhancer.findValidationApi((Class<D>) instance.getClass(), connectionSourceName)
-                        hasErrors = !validationApi.validate((D) instance, params)
-                    }
-                    else {
-                        hasErrors = !validateable.validate(params)
-                    }
-                }
-                else {
-                    hasErrors = !validateable.validate(params)
-                }
-                // don't revalidate
-                if (shouldFlush) {
-                    validateable.skipValidation(true)
-                }
-
-            } else {
-                validateable.skipValidation(true)
-                validateable.clearErrors()
-            }
-        }
-
-        if (hasErrors) {
-            boolean failOnErrorEnabled = params?.containsKey('failOnError') ? params.failOnError : failOnError
-            if (failOnErrorEnabled) {
-                throw validationException.newInstance('Validation error occurred during call to save()', InvokerHelper.getProperty(instance, 'errors'))
-            }
-            return null
-        }
-        if (isInsert) {
-            session.insert(instance)
-        }
-        else {
-            if (instance instanceof DirtyCheckable && markDirty) {
-                // since this is an explicit call to save() we mark the instance as dirty to ensure it happens
-                instance.markDirty()
-            }
-            session.persist(instance)
-        }
-        if (shouldFlush) {
-            session.flush()
-        }
-        return instance
-    }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApiRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApiRegistry.groovy
new file mode 100644
index 0000000000..e96b2786fa
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApiRegistry.groovy
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.model.MappingContext
+
+@CompileStatic
+class GormInstanceApiRegistry extends AbstractGormApiRegistry<GormInstanceApi> {
+
+    GormInstanceApiRegistry(GormRegistry registry) {
+        super(registry)
+    }
+
+    @Override
+    protected GormInstanceApi qualify(GormInstanceApi api, String qualifier) {
+        Class persistentClass = api.persistentClass
+        Datastore datastore = registry.apiResolver.findDatastore(persistentClass, qualifier)
+        if (datastore == null) {
+            return api
+        }
+        MappingContext mappingContext = datastore.mappingContext
+        DatastoreResolver resolver = registry.createClassDatastoreResolver(persistentClass, qualifier)
+        return registry.getApiFactory(datastore).createInstanceApi(persistentClass, mappingContext, resolver, registry, api.failOnError, api.markDirty)
+    }
+
+    <D> GormInstanceApi<D> findInstanceApi(Class<D> entity, String qualifier = null) {
+        String className = className(entity)
+        GormInstanceApi api = get(className)
+        if (api == null) {
+            throw stateException(entity)
+        }
+
+        if (qualifier != null && qualifier != ConnectionSource.DEFAULT) {
+            return api.forQualifier(qualifier)
+        }
+        return (GormInstanceApi<D>) api
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy
new file mode 100644
index 0000000000..14da3f82c4
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy
@@ -0,0 +1,898 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  'License'); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import java.util.concurrent.ConcurrentHashMap
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import org.springframework.transaction.PlatformTransactionManager
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.reflect.NameUtils
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
+
+/**
+ * A registry of GORM API objects. This registry is used to decouple the API
+ * objects from the static state in GormEnhancer.
+ *
+ * It implements an O(M+N) memory strategy where:
+ * M = Number of Entities
+ * N = Number of Connections (Tenants)
+ *
+ * @author Walter Duque de Estrada
+ * @since 8.0.0
+ */
+@Slf4j
+@CompileStatic
+class GormRegistry {
+
+    private static final GormRegistry instance = new GormRegistry()
+    private final GormApiFactory defaultApiFactory = new DefaultGormApiFactory()
+    final GormApiResolver apiResolver = new GormApiResolver(this)
+    final GormStaticApiRegistry staticApiRegistry = new GormStaticApiRegistry(this)
+    final GormInstanceApiRegistry instanceApiRegistry = new GormInstanceApiRegistry(this)
+    final GormValidationApiRegistry validationApiRegistry = new GormValidationApiRegistry(this)
+
+    final Map<String, Datastore> datastoresByQualifier = new ConcurrentHashMap<>()
+    private final Map<String, Map<String, Datastore>> entityDatastores = new ConcurrentHashMap<>()
+    private final Map<Class, String> normalizedEntityKeysByClass = new ConcurrentHashMap<>()
+    private final Map<String, String> normalizedEntityKeysByName = new ConcurrentHashMap<>()
+    private final Map<String, String> normalizedQualifiers = new ConcurrentHashMap<>()
+    final Map<Class, Datastore> datastoresByType = new ConcurrentHashMap<>()
+    private final Map<Class, GormApiFactory> apiFactoriesByDatastoreType = new ConcurrentHashMap<>()
+    final Set<Datastore> allDatastores = Collections.newSetFromMap(new ConcurrentHashMap<Datastore, Boolean>())
+
+    static GormRegistry getInstance() {
+        return instance
+    }
+
+    /**
+     * @return The default datastore
+     */
+    Datastore getDefaultDatastore() {
+        return datastoresByQualifier.get(ConnectionSource.DEFAULT)
+    }
+
+    /**
+     * Resets the registry.
+     * Nominally unused in core mapping runtime code, but heavily used by testing frameworks to reset state between spec executions.
+     */
+    static void reset() {
+        instance.resetInstance()
+    }
+
+    private void resetInstance() {
+        staticApiRegistry.clear()
+        instanceApiRegistry.clear()
+        validationApiRegistry.clear()
+        datastoresByQualifier.clear()
+        entityDatastores.clear()
+        normalizedEntityKeysByClass.clear()
+        normalizedEntityKeysByName.clear()
+        normalizedQualifiers.clear()
+        datastoresByType.clear()
+        apiFactoriesByDatastoreType.clear()
+        allDatastores.clear()
+        GormEnhancerRegistry.getInstance().clearPreferredDatastore()
+        GormEnhancerRegistry.getInstance().clearResolvingDatastoreDepth()
+    }
+
+    static <D> GormStaticApi<D> findStaticApi(Class<D> entity) {
+        instance.resolveStaticApi(entity, (String) null)
+    }
+
+    static <D> GormStaticApi<D> findStaticApi(Class<D> entity, String qualifier) {
+        instance.resolveStaticApi(entity, qualifier)
+    }
+
+    static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity) {
+        instance.resolveInstanceApi(entity, (String) null)
+    }
+
+    static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity, String qualifier) {
+        instance.resolveInstanceApi(entity, qualifier)
+    }
+
+    static <D> GormValidationApi<D> findValidationApi(Class<D> entity) {
+        instance.resolveValidationApi(entity, (String) null)
+    }
+
+    static <D> GormValidationApi<D> findValidationApi(Class<D> entity, String qualifier) {
+        instance.resolveValidationApi(entity, qualifier)
+    }
+
+    static Datastore findDatastore(Class entity) {
+        instance.apiResolver.findDatastore(entity, (String) null)
+    }
+
+    static Datastore findDatastore(Class entity, String qualifier) {
+        instance.apiResolver.findDatastore(entity, qualifier)
+    }
+
+    /**
+     * Registers a custom GormApiFactory for a specific datastore type.
+     * Nominally unused within the core mapping module, but invoked dynamically by external datastore implementations (e.g. Hibernate, MongoDB) to customize API generation.
+     */
+    void registerApiFactory(Class datastoreType, GormApiFactory factory) {
+        apiFactoriesByDatastoreType.put(datastoreType, factory)
+    }
+
+    GormApiFactory getApiFactory(Datastore datastore) {
+        GormApiFactory factory = apiFactoriesByDatastoreType.get(datastore.getClass())
+        if (factory == null) {
+            for (Map.Entry<Class, GormApiFactory> entry in apiFactoriesByDatastoreType.entrySet()) {
+                if (entry.key.isInstance(datastore)) {
+                    return entry.value
+                }
+            }
+            return defaultApiFactory
+        }
+        return factory
+    }
+
+    /**
+     * Finds a single transaction manager if only one datastore is registered.
+     * Nominally unused, but invoked at compile-time by transactional AST transformations.
+     */
+    PlatformTransactionManager findSingleTransactionManager() {
+        return findSingleTransactionManager(ConnectionSource.DEFAULT)
+    }
+
+    /**
+     * Finds a single transaction manager for a specific qualifier.
+     * Nominally unused, but invoked at compile-time by transactional AST transformations.
+     */
+    PlatformTransactionManager findSingleTransactionManager(String qualifier) {
+        Datastore ds = getDatastoreByString((String) null, qualifier)
+        if (ds == null) {
+            if (defaultDatastore == null) {
+                throw new IllegalStateException('No GORM implementations configured. Ensure GORM has been initialized correctly')
+            }
+            return null
+        }
+        if (ds instanceof TransactionCapableDatastore) {
+            return ((TransactionCapableDatastore) ds).transactionManager
+        }
+        return null
+    }
+
+    /**
+     * Finds a transaction manager for a specific entity class and qualifier.
+     * Nominally unused, but invoked at compile-time by transactional/service AST transformations.
+     */
+    PlatformTransactionManager findTransactionManager(Class entityClass, String qualifier) {
+        Datastore ds = getDatastore(entityClass, qualifier)
+        if (ds == null) {
+            // The qualifier may be a tenant ID rather than a registered datastore qualifier
+            // (e.g. DISCRIMINATOR / SCHEMA multi-tenancy). Fall back via the full resolver
+            // which understands the multi-tenancy mode and returns the correct datastore.
+            ds = apiResolver.findDatastore(entityClass, qualifier)
+        }
+        if (ds == null) {
+            if (defaultDatastore == null) {
+                throw new IllegalStateException('No GORM implementations configured. Ensure GORM has been initialized correctly')
+            }
+            return null
+        }
+        if (ds instanceof TransactionCapableDatastore) {
+            return ((TransactionCapableDatastore) ds).transactionManager
+        }
+        return null
+    }
+
+    /**
+     * Finds a transaction manager for a specific entity class.
+     * Nominally unused, but invoked at compile-time by transactional/service AST transformations.
+     */
+    PlatformTransactionManager findTransactionManager(Class entityClass) {
+        return findTransactionManager(entityClass, ConnectionSource.DEFAULT)
+    }
+
+    /**
+     * Finds a datastore for a specific qualifier (connection name).
+     */
+    Datastore getDatastore(String qualifier) {
+        return getDatastoreByString((String) null, qualifier)
+    }
+
+    /**
+     * Internal method to avoid redundant normalization.
+     */
+    Datastore getDatastoreDirect(String normalizedClassName, String normalizedQualifier) {
+        if (normalizedClassName != null) {
+            Map<String, Datastore> mappedDatastores = entityDatastores.get(normalizedClassName)
+            if (mappedDatastores != null) {
+                Datastore ds = mappedDatastores.get(normalizedQualifier)
+                if (ds != null) {
+                    return ds
+                }
+                if (ConnectionSource.DEFAULT.equals(normalizedQualifier) && !mappedDatastores.isEmpty()) {
+                    return mappedDatastores.values().iterator().next()
+                }
+                Datastore qualifierDs = datastoresByQualifier.get(normalizedQualifier)
+                if (qualifierDs != null && qualifierDs.getMappingContext()?.getPersistentEntity(normalizedClassName) != null) {
+                    return qualifierDs
+                }
+                return null
+            }
+        }
+
+        Datastore ds = datastoresByQualifier.get(normalizedQualifier)
+        if (ds == null && ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+            if (allDatastores.size() == 1) {
+                return allDatastores.iterator().next()
+            }
+        }
+        return ds
+    }
+
+    /**
+     * Internal method to avoid ambiguity.
+     */
+    Datastore getDatastoreByString(String className, String qualifier) {
+        return getDatastoreDirect(className != null ? normalizeEntityKey(className) : null, normalizeQualifier(qualifier))
+    }
+
+    /**
+     * Finds a datastore for a specific entity class.
+     * Part of the public API for external integrations and manual datastore lookup.
+     */
+    Datastore getDatastore(Class entityClass) {
+        return getDatastore(entityClass, ConnectionSource.DEFAULT)
+    }
+
+    /**
+     * Finds a datastore for a specific entity class and qualifier.
+     */
+    Datastore getDatastore(Class entityClass, String qualifier) {
+        return getDatastoreByString(entityClass != null ? normalizeEntityKey(entityClass) : (String) null, qualifier)
+    }
+
+    /**
+     * Finds a datastore for an entity class name and qualifier.
+     */
+    Datastore getDatastore(String className, String qualifier) {
+        return getDatastoreByString(className, qualifier)
+    }
+
+    /**
+     * Registers GORM APIs for an entity.
+     */
+    void registerApi(String className, GormStaticApi staticApi, GormInstanceApi instanceApi, GormValidationApi validationApi) {
+        String normalizedClassName = normalizeEntityKey(className)
+        staticApiRegistry.register(normalizedClassName, staticApi)
+        instanceApiRegistry.register(normalizedClassName, instanceApi)
+        validationApiRegistry.register(normalizedClassName, validationApi)
+    }
+
+    /**
+     * Registers a datastore for a qualifier. (O(N) part)
+     */
+    void registerDatastore(String qualifier, Datastore datastore) {
+        if (datastore == null) return
+        String normalizedQualifier = normalizeQualifier(qualifier)
+        datastoresByQualifier.put(normalizedQualifier, datastore)
+        allDatastores.add(datastore)
+    }
+
+    /**
+     * Initializes a datastore, registering its type and default qualifier.
+     */
+    void initializeDatastore(Datastore datastore) {
+        if (datastore == null) return
+        registerDatastore(ConnectionSource.DEFAULT, datastore)
+        datastoresByType.put(datastore.getClass(), datastore)
+    }
+
+    /**
+     * Registers a datastore.
+     */
+    void registerDatastore(Datastore datastore) {
+        initializeDatastore(datastore)
+    }
+
+    /**
+     * Registers a datastore by its type.
+     * Nominally unused in core mapping runtime code, but used by test suites and external integrations.
+     */
+    void registerDatastoreByType(Datastore datastore) {
+        if (datastore == null) return
+        datastoresByType.put(datastore.getClass(), datastore)
+        allDatastores.add(datastore)
+    }
+
+    /**
+     * Registers a datastore by qualifier only, without adding it to the global type-based discovery.
+     */
+    void registerDatastoreByQualifier(String qualifier, Datastore datastore) {
+        if (qualifier != null && datastore != null) {
+            datastoresByQualifier.put(normalizeQualifier(qualifier), datastore)
+        }
+    }
+
+    /**
+     * Removes a datastore from discovery by its class type.
+     * Nominally unused in core mapping runtime code, but used by testing frameworks to clean up dynamic datastores.
+     */
+    void removeDatastoreByType(Class datastoreType) {
+        if (datastoreType == null) return
+        datastoresByType.remove(datastoreType)
+    }
+
+    /**
+     * Removes a datastore from discovery by its instance type.
+     * Nominally unused in core mapping runtime code, but used by testing frameworks to clean up dynamic datastores.
+     */
+    void removeDatastoreByType(Datastore datastore) {
+        if (datastore == null) return
+        removeDatastoreByType(datastore.getClass())
+    }
+
+    /**
+     * Removes a datastore from global discovery (allDatastores and datastoresByType)
+     * but keeps it in datastoresByQualifier.
+     * Nominally unused in core mapping runtime code, but used by test suites to verify multi-datastore isolation.
+     */
+    void removeDatastoreFromDiscovery(Datastore datastore) {
+        if (datastore == null) return
+        allDatastores.remove(datastore)
+        datastoresByType.remove(datastore.getClass())
+    }
+
+    /**
+     * Completely removes a datastore from the registry.
+     */
+    void removeDatastore(Datastore datastore) {
+        if (datastore == null) return
+        allDatastores.remove(datastore)
+        datastoresByType.remove(datastore.getClass())
+
+        Iterator<Map.Entry<String, Datastore>> it = datastoresByQualifier.entrySet().iterator()
+        while (it.hasNext()) {
+            if (it.next().value == datastore) it.remove()
+        }
+
+        for (Map<String, Datastore> entityMap in entityDatastores.values()) {
+            Iterator<Map.Entry<String, Datastore>> eit = entityMap.entrySet().iterator()
+            while (eit.hasNext()) {
+                if (eit.next().value == datastore) eit.remove()
+            }
+        }
+
+        staticApiRegistry.removeDatastore(datastore)
+        instanceApiRegistry.removeDatastore(datastore)
+        validationApiRegistry.removeDatastore(datastore)
+    }
+
+    /**
+     * Removes a datastore for a specific entity.
+     */
+    void removeEntityDatastore(String className, Datastore datastore) {
+        if (className != null && datastore != null) {
+            Map<String, Datastore> entityMap = entityDatastores.get(className)
+            if (entityMap != null) {
+                Iterator<Map.Entry<String, Datastore>> eit = entityMap.entrySet().iterator()
+                while (eit.hasNext()) {
+                    if (eit.next().value == datastore) eit.remove()
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if a specific datastore is explicitly registered for an entity.
+     */
+    boolean isDatastoreRegisteredForEntity(String className, Datastore datastore) {
+        if (className != null && datastore != null) {
+            Map<String, Datastore> entityMap = entityDatastores.get(normalizeEntityKey(className))
+            if (entityMap != null && entityMap.values().contains(datastore)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    GormStaticApi getStaticApi(Class entityClass) {
+        return staticApiRegistry.get(normalizeEntityKey(entityClass))
+    }
+
+    GormInstanceApi getInstanceApi(Class entityClass) {
+        return instanceApiRegistry.get(normalizeEntityKey(entityClass))
+    }
+
+    GormValidationApi getValidationApi(Class entityClass) {
+        return validationApiRegistry.get(normalizeEntityKey(entityClass))
+    }
+
+    GormStaticApi getStaticApi(Class entityClass, String qualifier) {
+        return staticApiRegistry.get(normalizeEntityKey(entityClass), normalizeQualifier(qualifier))
+    }
+
+    GormInstanceApi getInstanceApi(Class entityClass, String qualifier) {
+        return instanceApiRegistry.get(normalizeEntityKey(entityClass), normalizeQualifier(qualifier))
+    }
+
+    GormValidationApi getValidationApi(Class entityClass, String qualifier) {
+        return validationApiRegistry.get(normalizeEntityKey(entityClass), normalizeQualifier(qualifier))
+    }
+
+    GormStaticApi resolveStaticApi(Class entityClass) {
+        return resolveStaticApi(entityClass, (String) null)
+    }
+
+    GormStaticApi resolveStaticApi(Class entityClass, String qualifier) {
+        String normalizedClassName = normalizeEntityKey(entityClass)
+        String normalizedQualifier = normalizeQualifier(qualifier)
+
+        if (MultiTenant.isAssignableFrom(entityClass)) {
+            // Priority 1: Explicit qualifier that doesn't match default is likely a tenant ID
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormStaticApi api = staticApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+                if (api != null) return api
+            }
+
+            // Priority 2: Check current bound tenant if using default qualifier
+            Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
+            if (ds instanceof MultiTenantCapableDatastore) {
+                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
+                if (tenantId != null) {
+                    GormStaticApi api = staticApiRegistry.getDirect(normalizedClassName, tenantId.toString())
+                    if (api != null) return api
+                }
+            }
+            
+            // Priority 3: Fall back to default API instance if specialized one not found,
+            // but keep the qualifier so the API can handle tenant binding
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormStaticApi api = staticApiRegistry.getDirect(normalizedClassName, ConnectionSource.DEFAULT)
+                if (api != null) return api
+            }
+        }
+
+        return staticApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+    }
+
+    GormInstanceApi resolveInstanceApi(Class entityClass) {
+        return resolveInstanceApi(entityClass, (String) null)
+    }
+
+    GormInstanceApi resolveInstanceApi(Class entityClass, String qualifier) {
+        String normalizedClassName = normalizeEntityKey(entityClass)
+        String normalizedQualifier = normalizeQualifier(qualifier)
+
+        if (MultiTenant.isAssignableFrom(entityClass)) {
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormInstanceApi api = instanceApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+                if (api != null) return api
+            }
+
+            Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
+            if (ds instanceof MultiTenantCapableDatastore) {
+                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
+                if (tenantId != null) {
+                    GormInstanceApi api = instanceApiRegistry.getDirect(normalizedClassName, tenantId.toString())
+                    if (api != null) return api
+                }
+            }
+            
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormInstanceApi api = instanceApiRegistry.getDirect(normalizedClassName, ConnectionSource.DEFAULT)
+                if (api != null) return api
+            }
+        }
+
+        return instanceApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+    }
+
+    /**
+     * Resolves the validation API for the given entity class.
+     * Nominally unused in core mapping runtime code, but invoked at compile-time by GORM's AST transformations.
+     */
+    GormValidationApi resolveValidationApi(Class entityClass) {
+        return resolveValidationApi(entityClass, (String) null)
+    }
+
+    /**
+     * Resolves the validation API for the given entity class and qualifier.
+     * Nominally unused in core mapping runtime code, but invoked at compile-time by GORM's AST transformations.
+     */
+    GormValidationApi resolveValidationApi(Class entityClass, String qualifier) {
+        String normalizedClassName = normalizeEntityKey(entityClass)
+        String normalizedQualifier = normalizeQualifier(qualifier)
+
+        if (MultiTenant.isAssignableFrom(entityClass)) {
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormValidationApi api = validationApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+                if (api != null) return api
+            }
+
+            Datastore ds = getDatastoreDirect(normalizedClassName, normalizedQualifier)
+            if (ds instanceof MultiTenantCapableDatastore) {
+                Serializable tenantId = CurrentTenantHolder.get((MultiTenantCapableDatastore) ds)
+                if (tenantId != null) {
+                    GormValidationApi api = validationApiRegistry.getDirect(normalizedClassName, tenantId.toString())
+                    if (api != null) return api
+                }
+            }
+            
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                GormValidationApi api = validationApiRegistry.getDirect(normalizedClassName, ConnectionSource.DEFAULT)
+                if (api != null) return api
+            }
+        }
+
+        return validationApiRegistry.getDirect(normalizedClassName, normalizedQualifier)
+    }
+
+    GormStaticApi getStaticApi(String className) {
+        return staticApiRegistry.get(normalizeEntityKey(className))
+    }
+
+    GormStaticApi getStaticApi(String className, String qualifier) {
+        return staticApiRegistry.get(normalizeEntityKey(className), normalizeQualifier(qualifier))
+    }
+
+    GormInstanceApi getInstanceApi(String className) {
+        return instanceApiRegistry.get(normalizeEntityKey(className))
+    }
+
+    GormInstanceApi getInstanceApi(String className, String qualifier) {
+        return instanceApiRegistry.get(normalizeEntityKey(className), normalizeQualifier(qualifier))
+    }
+
+    GormValidationApi getValidationApi(String className) {
+        return validationApiRegistry.get(normalizeEntityKey(className))
+    }
+
+    GormValidationApi getValidationApi(String className, String qualifier) {
+        return validationApiRegistry.get(normalizeEntityKey(className), normalizeQualifier(qualifier))
+    }
+
+    private Map<String, Datastore> getInternalMap(Map<String, Map<String, Datastore>> rootMap, String key) {
+        Map<String, Datastore> map = rootMap.get(key)
+        if (map == null) {
+            map = new ConcurrentHashMap<String, Datastore>()
+            Map<String, Datastore> prior = rootMap.putIfAbsent(key, map)
+            if (prior != null) {
+                return prior
+            }
+        }
+        return map
+    }
+
+    String normalizeEntityKey(Object entityKey) {
+        if (entityKey == null) {
+            return null
+        }
+        if (entityKey instanceof Class) {
+            Class entityClass = (Class) entityKey
+            String existing = normalizedEntityKeysByClass.get(entityClass)
+            if (existing != null) {
+                return existing
+            }
+            String computed = NameUtils.getClassName(entityClass)
+            String normalized = normalizeEntityKey(computed)
+            if (normalized == null) {
+                return null
+            }
+            String prior = normalizedEntityKeysByClass.putIfAbsent(entityClass, normalized)
+            return prior != null ? prior : normalized
+        } else {
+            String className = entityKey.toString()
+            String existing = normalizedEntityKeysByName.get(className)
+            if (existing != null) {
+                return existing
+            }
+            String normalized = className.trim()
+            if (normalized.isEmpty()) {
+                return null
+            }
+            String prior = normalizedEntityKeysByName.putIfAbsent(className, normalized)
+            return prior != null ? prior : normalized
+        }
+    }
+
+    /**
+     * @deprecated Use {@code normalizeEntityKey(Class)}.
+     */
+    @Deprecated
+    String normalizeEntityKeyFromClass(Class entityClass) {
+        normalizeEntityKey(entityClass)
+    }
+
+    String normalizeQualifier(String qualifier) {
+        if (qualifier == null) {
+            return ConnectionSource.DEFAULT
+        }
+        String existing = normalizedQualifiers.get(qualifier)
+        if (existing != null) {
+            return existing
+        }
+        String normalized = qualifier.trim()
+        if (normalized.isEmpty() || ConnectionSource.OLD_DEFAULT.equalsIgnoreCase(normalized)) {
+            normalized = ConnectionSource.DEFAULT
+        }
+        String prior = normalizedQualifiers.putIfAbsent(qualifier, normalized)
+        return prior != null ? prior : normalized
+    }
+
+    /**
+     * @deprecated Use {@code normalizeQualifier(String)}.
+     */
+    @Deprecated
+    String normalizeQualifierByString(String qualifier) {
+        normalizeQualifier(qualifier)
+    }
+
+    /**
+     * Register API objects for a persistent entity.
+     * Creates and registers StaticApi, InstanceApi, and ValidationApi for the given entity.
+     * Part of the public API for external plugins and test environments.
+     *
+     * @param className The entity class name
+     * @param staticApi The static API implementation
+     * @param instanceApi The instance API implementation
+     * @param validationApi The validation API implementation
+     */
+    void registerEntityApis(String className, GormStaticApi staticApi, GormInstanceApi instanceApi, GormValidationApi validationApi) {
+        registerApi(className, staticApi, instanceApi, validationApi)
+    }
+
+    /**
+     * Register datastores for a persistent entity across multiple connection sources.
+     * Handles entity-specific datastore mappings for multi-tenant and multi-datasource scenarios.
+     *
+     * @param className The entity class name
+     * @param datastore The datastore to register
+     * @param connectionSourceNames The connection source names to register the datastore for
+     * @param entity The persistent entity (for entity-specific qualifier resolution)
+     */
+    void registerEntityDatastores(String className, Object datastore, List<String> connectionSourceNames, Object entity) {
+
+        if (datastore == null) return
+        String normalizedClassName = normalizeEntityKey(className)
+        if (normalizedClassName == null) {
+            return
+        }
+
+        Datastore defaultDatastore = (Datastore) datastore
+        List<String> qualifiers = connectionSourceNames ?: Collections.singletonList(ConnectionSource.DEFAULT)
+        boolean multiTenantEntity = entity instanceof PersistentEntity && ((PersistentEntity) entity).isMultiTenant()
+
+        entityDatastores.remove(normalizedClassName)
+
+        Datastore primaryDatastore = defaultDatastore
+
+        // Register datastores for each connection source. For each qualifier, attempt to resolve a
+        // connection-specific child datastore. If the resolution falls back to the parent (meaning
+        // the qualifier is a runtime tenant ID, not a datasource connection name), skip registration
+        // for non-DEFAULT qualifiers in multi-tenant mode so that we do not overwrite the correctly
+        // registered child datastores (e.g. those added by addTenantForSchemaInternal).
+        for (String connectionSourceName in qualifiers) {
+            String normalizedQualifier = normalizeQualifier(connectionSourceName)
+            Datastore qualifierDatastore = defaultDatastore
+            if (defaultDatastore instanceof MultipleConnectionSourceCapableDatastore &&
+                    !ConnectionSource.DEFAULT.equals(normalizedQualifier)) {
+                try {
+                    Datastore resolved = ((MultipleConnectionSourceCapableDatastore) defaultDatastore)
+                            .getDatastoreForConnection(normalizedQualifier)
+                    if (resolved != null) {
+                        qualifierDatastore = resolved
+                    }
+                } catch (Throwable e) {
+                    // qualifier is not a datasource connection name; keep defaultDatastore
+                }
+            }
+            // Skip non-DEFAULT qualifiers that resolve back to the parent for multi-tenant entities.
+            // Those qualifiers are runtime tenant IDs handled at the session level, not datasource names.
+            if (multiTenantEntity && !ConnectionSource.DEFAULT.equals(normalizedQualifier) &&
+                    qualifierDatastore == defaultDatastore) {
+                continue
+            }
+            if (!ConnectionSource.DEFAULT.equals(normalizedQualifier) && primaryDatastore == defaultDatastore) {
+                primaryDatastore = qualifierDatastore
+            }
+            registerDatastoreByQualifier(normalizedQualifier, qualifierDatastore)
+            registerEntityDatastore(normalizedClassName, normalizedQualifier, qualifierDatastore)
+        }
+
+        // If the entity does not explicitly include DEFAULT, route DEFAULT to the first explicit connection.
+        if (!qualifiers.collect { String it -> normalizeQualifier(it) }.contains(ConnectionSource.DEFAULT)) {
+            registerEntityDatastore(normalizedClassName, ConnectionSource.DEFAULT, primaryDatastore)
+        }
+    }
+
+    /**
+     * Registers an entity-specific datastore override.
+     */
+    void registerEntityDatastore(String className, String qualifier, Datastore datastore) {
+        if (datastore != null) {
+            String normalizedClassName = normalizeEntityKey(className)
+            if (normalizedClassName == null) {
+                return
+            }
+            String normalizedQualifier = normalizeQualifier(qualifier)
+            getInternalMap(entityDatastores, normalizedClassName).put(normalizedQualifier, datastore)
+        }
+    }
+
+    /**
+     * Creates dynamic finders for the default datastore
+     *
+     * @return List of finder methods
+     */
+    List<FinderMethod> createDynamicFinders(Datastore targetDatastore) {
+        createDynamicFinders(new DatastoreResolver() {
+            @Override
+            Datastore resolve() {
+                targetDatastore
+            }
+        }, targetDatastore.getMappingContext())
+    }
+
+    /**
+     * Creates dynamic finders using the given resolver and mapping context.
+     *
+     * @param resolver The datastore resolver
+     * @param mappingContext The mapping context
+     * @return List of finder methods
+     */
+    List<FinderMethod> createDynamicFinders(DatastoreResolver resolver, MappingContext mappingContext) {
+        Datastore ds = resolver.resolve()
+        if (ds != null) {
+            return getApiFactory(ds).createDynamicFinders(resolver, mappingContext)
+        }
+        return []
+    }
+
+    /**
+     * Create a DatastoreResolver for a class and optional qualifier.
+     */
+    DatastoreResolver createClassDatastoreResolver(Class cls, String qualifier = ConnectionSource.DEFAULT) {
+        String normalizedClassName = normalizeEntityKey(cls)
+        String normalizedQualifier = normalizeQualifier(qualifier)
+        return new DatastoreResolver() {
+            @Override
+            Datastore resolve() {
+                apiResolver.findDatastore(cls, normalizedQualifier)
+            }
+        }
+    }
+
+    /**
+     * Create a GormStaticApi instance
+     */
+    GormStaticApi createStaticApi(Class cls, Datastore datastore, DatastoreResolver resolver, String qualifier) {
+        return getApiFactory(datastore).createStaticApi(cls, datastore.mappingContext, resolver, qualifier, this)
+    }
+
+    /**
+     * Create a GormInstanceApi instance
+     */
+    GormInstanceApi createInstanceApi(Class cls, Datastore datastore, DatastoreResolver resolver, boolean failOnError, boolean markDirty) {
+        return getApiFactory(datastore).createInstanceApi(cls, datastore.mappingContext, resolver, this, failOnError, markDirty)
+    }
+
+    /**
+     * Create a GormValidationApi instance
+     */
+    GormValidationApi createValidationApi(Class cls, Datastore datastore, DatastoreResolver resolver) {
+        return getApiFactory(datastore).createValidationApi(cls, datastore.mappingContext, resolver, this)
+    }
+
+    /**
+     * Register API objects for a persistent entity.
+     * Part of the public API for external plugins and test environments.
+     */
+    void registerEntityApis(Class cls, GormStaticApi staticApi, GormInstanceApi instanceApi, GormValidationApi validationApi) {
+        registerEntityApis(cls.name, staticApi, instanceApi, validationApi)
+    }
+
+    /**
+     * Register constraints for all entities in a datastore.
+     * Delegates to the ConstraintsEvaluator if available in the mapping context.
+     *
+     * @param datastore The datastore containing the entities
+     */
+    @CompileDynamic
+    void registerConstraints(Object datastore) {
+        if (datastore == null) return
+        
+        try {
+            def context = ((Datastore) datastore).mappingContext
+            def factory = context.mappingFactory
+            if (factory.hasProperty('entityContext')) {
+                def constraintsEvaluator = factory.entityContext.getBean(Class.forName('org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator', false, GormRegistry.classLoader))
+                if (constraintsEvaluator != null) {
+                    for (entity in context.persistentEntities) {
+                        constraintsEvaluator.evaluate(entity.javaClass)
+                    }
+                }
+            }
+        } catch (Throwable e) {
+            log.debug('Could not register GORM constraints: {}', e.message)
+        }
+    }
+
+    /**
+     * Initialize a datastore with GORM.
+     * Orchestrates constraint registration and datastore registration.
+     * Note: Entity-specific registration is still handled by GormEnhancer.
+     *
+     * @param datastore The datastore to initialize
+     * @param defaultQualifier The default connection source qualifier
+     */
+    void initializeDatastore(Object datastore, String defaultQualifier) {
+        if (datastore == null) return
+        
+        // Register constraints
+        registerConstraints(datastore)
+        
+        // Register datastore with default qualifier
+        Datastore typedDatastore = (Datastore) datastore
+        registerDatastore(defaultQualifier, typedDatastore)
+        datastoresByType.put(typedDatastore.getClass(), typedDatastore)
+    }
+
+    /**
+     * Register a persistent entity with GORM, orchestrating API and datastore registration.
+     * This delegates to a GormEnhancer for creating the API instances.
+     *
+     * @param entity The persistent entity to register
+     * @param enhancer The GormEnhancer that provides API creation
+     */
+    void registerEntity(PersistentEntity persistentEntity, GormEnhancer enhancer) {
+        if (!persistentEntity) {
+            throw new IllegalArgumentException('Argument [persistentEntity] cannot be null')
+        }
+        if (!enhancer) {
+            throw new IllegalArgumentException('Argument [enhancer] cannot be null')
+        }
+
+        String className = persistentEntity.name
+
+        // Always (re)register API singletons so classloader or datastore changes do not leave stale API instances.
+        final Class cls = persistentEntity.javaClass
+        DatastoreResolver resolver = createClassDatastoreResolver(cls)
+        Datastore datastore = enhancer.datastore
+
+        GormStaticApi staticApi = createStaticApi(cls, datastore, resolver, ConnectionSource.DEFAULT)
+        GormInstanceApi instanceApi = createInstanceApi(cls, datastore, resolver, enhancer.failOnError, enhancer.markDirty)
+        GormValidationApi validationApi = createValidationApi(cls, datastore, resolver)
+
+        registerEntityApis(className, staticApi, instanceApi, validationApi)
+
+        // Register datastore mappings
+        Datastore datastoreForMappings = enhancer.datastore
+        List<String> qualifiers = enhancer.allQualifiers(datastore, persistentEntity)
+        registerEntityDatastores(className, datastoreForMappings, qualifiers, persistentEntity)
+    }
+
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy
index 99c54eb671..6520e744e4 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy
@@ -19,1185 +19,866 @@
 package org.grails.datastore.gorm
 
 import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.transform.TypeCheckingMode
-import org.codehaus.groovy.runtime.InvokerHelper
+import groovy.util.logging.Slf4j
 
-import org.springframework.beans.PropertyAccessorFactory
-import org.springframework.beans.factory.config.AutowireCapableBeanFactory
 import org.springframework.transaction.PlatformTransactionManager
 import org.springframework.transaction.TransactionDefinition
 import org.springframework.transaction.support.DefaultTransactionDefinition
-import org.springframework.util.Assert
 
 import grails.gorm.CriteriaBuilder
 import grails.gorm.DetachedCriteria
-import grails.gorm.MultiTenant
-import grails.gorm.PagedResultList
 import grails.gorm.api.GormAllOperations
+import grails.gorm.api.GormInstanceOperations
+import grails.gorm.api.GormStaticOperations
 import grails.gorm.multitenancy.Tenants
 import grails.gorm.transactions.GrailsTransactionTemplate
-import org.grails.datastore.gorm.finders.DynamicFinder
 import org.grails.datastore.gorm.finders.FinderMethod
-import org.grails.datastore.gorm.multitenancy.TenantDelegatingGormOperations
-
+import org.grails.datastore.gorm.transactions.DefaultTransactionTemplateFactory
+import org.grails.datastore.gorm.transactions.TransactionTemplateFactory
 import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.datastore.mapping.core.Session
 import org.grails.datastore.mapping.core.SessionCallback
-import org.grails.datastore.mapping.core.StatelessDatastore
 import org.grails.datastore.mapping.core.connections.ConnectionSource
-import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
 import org.grails.datastore.mapping.core.connections.ConnectionSources
 import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
+import org.grails.datastore.mapping.model.MappingContext
 import org.grails.datastore.mapping.model.PersistentEntity
-import org.grails.datastore.mapping.model.PersistentProperty
-import org.grails.datastore.mapping.model.types.Association
-import org.grails.datastore.mapping.multitenancy.MultiTenancySettings.MultiTenancyMode
-import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
 import org.grails.datastore.mapping.query.api.BuildableCriteria
-import org.grails.datastore.mapping.query.api.Criteria
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
 /**
- * Static methods of the GORM API.
+ * Static methods for GORM
  *
  * @author Graeme Rocher
- * @param <D> the entity/domain class
  */
-@CompileStatic
+@CompileDynamic
+@Slf4j
 class GormStaticApi<D> extends AbstractGormApi<D> implements GormAllOperations<D> {
 
-    protected final List<FinderMethod> gormDynamicFinders
+    private static final TransactionTemplateFactory DEFAULT_TRANSACTION_TEMPLATE_FACTORY = new DefaultTransactionTemplateFactory()
+
+    protected final List<FinderMethod> finders
+
+    GormStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders) {
+        this(persistentClass, mappingContext, finders, null, ConnectionSource.DEFAULT, null)
+    }
 
-    protected final PlatformTransactionManager transactionManager
-    protected final String defaultQualifier
-    protected final MultiTenancyMode multiTenancyMode
-    protected final ConnectionSources connectionSources
+    GormStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders, String qualifier) {
+        this(persistentClass, mappingContext, finders, null, qualifier, null)
+    }
 
-    GormStaticApi(Class<D> persistentClass, Datastore datastore, List<FinderMethod> finders) {
-        this(persistentClass, datastore, finders, null)
+    GormStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders, DatastoreResolver resolver, String qualifier) {
+        this(persistentClass, mappingContext, finders, resolver, qualifier, null)
     }
 
-    GormStaticApi(Class<D> persistentClass, Datastore datastore, List<FinderMethod> finders, PlatformTransactionManager transactionManager) {
-        super(persistentClass, datastore)
-        gormDynamicFinders = finders
-        this.transactionManager = transactionManager
-        String qualifier = ConnectionSource.DEFAULT
-        if (datastore instanceof ConnectionSourcesProvider) {
-            this.connectionSources = ((ConnectionSourcesProvider) datastore).connectionSources
-            ConnectionSource<?, ? extends ConnectionSourceSettings> defaultConnectionSource = connectionSources.defaultConnectionSource
-            qualifier = defaultConnectionSource.name
-            multiTenancyMode = defaultConnectionSource.settings.multiTenancy.mode
+    GormStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders, DatastoreResolver resolver, String qualifier, GormRegistry registry) {
+        super(persistentClass, mappingContext, resolver, qualifier, registry)
+        this.finders = finders
+    }
 
+    @Override
+    PlatformTransactionManager getTransactionManager() {
+        Datastore ds = getDatastore()
+        if (ds instanceof TransactionCapableDatastore) {
+            return ((TransactionCapableDatastore)ds).getTransactionManager()
         }
-        else {
-            connectionSources = null
-            multiTenancyMode = MultiTenancyMode.NONE
+        return null
+    }
+
+    @Override
+    protected <T1> T1 executeQualified(String qualifier, SessionCallback<T1> callback) {
+        GormStaticApi<D> qualifiedApi = registry.findStaticApi(persistentClass, qualifier)
+        if (qualifiedApi != null && qualifiedApi != this) {
+            return (T1) qualifiedApi.execute(callback)
         }
-        this.defaultQualifier = qualifier
+        return DatastoreUtils.execute(getDatastore(), callback)
     }
 
-    /**
-     * @return The PersistentEntity for this class
-     */
+    @Override
     PersistentEntity getGormPersistentEntity() {
-        persistentEntity
+        PersistentEntity entity = qualifier != null ? registry.apiResolver.findEntity(persistentClass, qualifier) : null
+        if (entity == null) {
+            entity = super.getGormPersistentEntity()
+        }
+        if (entity == null) {
+            entity = registry.apiResolver.findEntity(persistentClass)
+        }
+        // Final fallback: resolve from the mapping context captured when this API was constructed.
+        // Entity metadata is identical across tenants/connections (only the datastore/session differs),
+        // so this stable reference is the most reliable source and avoids a null entity when runtime
+        // registry resolution is disturbed by cross-spec state — e.g. a qualified API created by
+        // withTenant(tenantId) for DISCRIMINATOR/SCHEMA multi-tenancy.
+        if (entity == null && mappingContext != null) {
+            entity = mappingContext.getPersistentEntity(persistentClass.name)
+        }
+        entity
     }
 
+    @Override
     List<FinderMethod> getGormDynamicFinders() {
-        gormDynamicFinders
+        return finders
     }
 
-    /**
-     * Property missing handler
-     *
-     * @param name The name of the property
-     */
-    def propertyMissing(String name) {
-        if (datastore instanceof ConnectionSourcesProvider) {
-            return GormEnhancer.findStaticApi(persistentClass, name)
-        }
-        else {
-            throw new MissingPropertyException(name, persistentClass)
+    GormStaticApi<D> forQualifier(String qualifier) {
+        Datastore ds = getDatastore()
+        DatastoreResolver resolver = new DatastoreResolver() {
+            @Override Datastore resolve() { registry.apiResolver.findDatastore(persistentClass, qualifier) }
         }
+        List<FinderMethod> qualifiedFinders = registry.createDynamicFinders(resolver, ds.mappingContext)
+        createStaticApi(persistentClass, ds.mappingContext, qualifiedFinders, resolver, qualifier)
     }
 
-    /**
-     * Property missing handler
-     *
-     * @param name The name of the property
-     */
-    void propertyMissing(String name, value) {
-        throw new MissingPropertyException(name, persistentClass)
+    protected GormStaticApi<D> createStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders, DatastoreResolver resolver, String qualifier) {
+        new GormStaticApi<D>(persistentClass, mappingContext, finders, resolver, qualifier, registry)
     }
 
-    /**
-     * Method missing handler that deals with the invocation of dynamic finders
-     *
-     * @param methodName The method name
-     * @param args The arguments
-     * @return The result of the method call
-     */
-    @CompileDynamic
-    def methodMissing(String methodName, Object args) {
-        FinderMethod method = gormDynamicFinders.find { FinderMethod f -> f.isMethodMatch(methodName) }
-        if (!method) {
-            throw new MissingMethodException(methodName, persistentClass, args)
+    @Override
+    Object methodMissing(String name, Object args) {
+        Object[] argsArray = (args instanceof Object[]) ? (Object[]) args : ([args] as Object[])
+        for (FinderMethod fm : finders) {
+            if (fm.isMethodMatch(name)) {
+                return execute({ Session session ->
+                    fm.invoke(persistentClass, name, argsArray)
+                } as SessionCallback)
+            }
         }
+        throw new MissingMethodException(name, persistentClass, argsArray)
+    }
 
-        // if the class is multi tenant, don't cache the method because the tenant will need to be resolved
-        // for each method call
-        if (!MultiTenant.isAssignableFrom(persistentClass)) {
-
-            def mc = persistentClass.getMetaClass()
-
-            // register the method invocation for next time
-            mc.static."$methodName" = { Object[] varArgs ->
-                // FYI... This is relevant to http://jira.grails.org/browse/GRAILS-3463 and may
-                // become problematic if http://jira.codehaus.org/browse/GROOVY-5876 is addressed...
-                final argumentsForMethod
-                if (varArgs == null) {
-                    argumentsForMethod = [null] as Object[]
+    @Override
+    Object propertyMissing(String name) {
+        for (FinderMethod fm : finders) {
+            if (fm.isMethodMatch(name)) {
+                return { Object... args ->
+                    Object[] finderArgs = args == null ? ([null] as Object[]) : args
+                    execute({ Session session ->
+                        fm.invoke(persistentClass, name, finderArgs)
+                    } as SessionCallback)
                 }
-                // if the argument component type is not an Object then we have an array passed that is the actual argument
-                else if (varArgs.getClass().componentType != Object) {
-                    // so we wrap it in an object array
-                    argumentsForMethod = [varArgs] as Object[]
+            }
+        }
+        
+        Datastore ds = getDatastore()
+        if (ds instanceof ConnectionSourcesProvider) {
+            ConnectionSources sources = ((ConnectionSourcesProvider) ds).connectionSources
+            if (sources != null) {
+                if (sources.getConnectionSource(name) != null) {
+                    return registry.findStaticApi(persistentClass, name)
                 }
-                else {
-
-                    if (varArgs.length == 1 && varArgs[0].getClass().isArray()) {
-                        argumentsForMethod = varArgs[0]
-                    } else {
-
-                        argumentsForMethod = varArgs
-                    }
+                if (name.equalsIgnoreCase(ConnectionSource.DEFAULT) || name.equalsIgnoreCase(ConnectionSource.OLD_DEFAULT)) {
+                    return registry.findStaticApi(persistentClass, ConnectionSource.DEFAULT)
                 }
-                method.invoke(delegate, methodName, argumentsForMethod)
             }
         }
+        // Fallback: the preferred/transactional datastore may be a single-datasource datastore
+        // that doesn't expose the named qualifier in its connectionSources. Check the registry
+        // directly so that entities mapped to multiple datasources (e.g. datasource 'ALL') can
+        // still be accessed via the qualifier even when a single-datasource transaction is active.
+        if (registry.getDatastoreByString(persistentClass.name, name) != null) {
+            return registry.findStaticApi(persistentClass, name)
+        }
+        throw new MissingPropertyException(name, persistentClass)
+    }
 
-        return method.invoke(persistentClass, methodName, args)
-    }
-
-    /**
-     *
-     * @param callable Callable closure containing detached criteria definition
-     * @return The DetachedCriteria instance
-     */
-    DetachedCriteria<D> where(Closure callable) {
-        new DetachedCriteria<D>(persistentClass).build(callable)
-    }
-
-    /**
-     *
-     * @param callable Callable closure containing detached criteria definition
-     * @return The DetachedCriteria instance that is lazily initialized
-     */
-    DetachedCriteria<D> whereLazy(Closure callable) {
-        new DetachedCriteria<D>(persistentClass).buildLazy(callable)
-    }
-    /**
-     *
-     * @param callable Callable closure containing detached criteria definition
-     * @return The DetachedCriteria instance
-     */
-    DetachedCriteria<D> whereAny(Closure callable) {
-        (DetachedCriteria<D>) new DetachedCriteria<D>(persistentClass).or(callable)
-    }
-
-    /**
-     * Uses detached criteria to build a query and then execute it returning a list
-     *
-     * @param callable The callable
-     * @return A List of entities
-     */
-    List<D> findAll(Closure callable) {
-        def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
-        return criteria.list()
+    @Override
+    void propertyMissing(String name, Object val) {
+        throw new MissingPropertyException(name, persistentClass)
     }
 
-    /**
-     * Uses detached criteria to build a query and then execute it returning a list
-     *
-     * @param args pagination parameters
-     * @param callable The callable
-     * @return A List of entities
-     */
-    List<D> findAll(Map args, Closure callable) {
-        def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
-        return criteria.list(args)
+    // GormInstanceOperations delegation
+    @Override
+    def propertyMissing(D instance, String name) {
+        registry.findInstanceApi(persistentClass, null).propertyMissing(instance, name)
     }
 
-    /**
-     * Uses detached criteria to build a query and then execute it returning a list
-     *
-     * @param callable The callable
-     * @return A single entity
-     */
-    D find(Closure callable) {
-        def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
-        return criteria.find()
+    @Override
+    boolean instanceOf(D instance, Class cls) {
+        registry.findInstanceApi(persistentClass, null).instanceOf(instance, cls)
     }
 
-    /**
-     * Saves a list of objects in one go
-     * @param objectsToSave The objects to save
-     * @return A list of object identifiers
-     */
-    List<Serializable> saveAll(Object... objectsToSave) {
-        (List<Serializable>) execute({ Session session ->
-            session.persist(Arrays.asList(objectsToSave))
-        } as SessionCallback)
+    @Override
+    D lock(D instance) {
+        registry.findInstanceApi(persistentClass, null).lock(instance)
     }
 
-    /**
-     * Saves a list of objects in one go
-     * @param objectToSave Collection of objects to save
-     * @return A list of object identifiers
-     */
-    List<Serializable> saveAll(Iterable<?> objectsToSave) {
-        (List<Serializable>) execute({ Session session ->
-            session.persist(objectsToSave)
-        } as SessionCallback)
+    @Override
+    def <T1> T1 mutex(D instance, Closure<T1> callable) {
+        registry.findInstanceApi(persistentClass, null).mutex(instance, callable)
     }
 
-    /**
-     * Deletes a list of objects in one go
-     * @param objectsToDelete The objects to delete
-     */
-    void deleteAll(Object... objectsToDelete) {
-        execute({ Session session ->
-            session.delete(Arrays.asList(objectsToDelete))
-        } as SessionCallback)
+    @Override
+    D refresh(D instance) {
+        registry.findInstanceApi(persistentClass, null).refresh(instance)
     }
 
-    /**
-     * Deletes a list of objects in one go and flushes when param is set
-     * @param objectsToDelete The objects to delete
-     */
-    void deleteAll(Map params, Object... objectsToDelete) {
-        execute({ Session session ->
-            session.delete(Arrays.asList(objectsToDelete))
-            if (params?.flush) {
-                session.flush()
-            }
-        } as SessionCallback)
+    @Override
+    D save(D instance) {
+        registry.findInstanceApi(persistentClass, null).save(instance)
     }
 
-    /**
-     * Deletes a list of objects in one go
-     * @param objectsToDelete Collection of objects to delete
-     */
-    void deleteAll(Iterable objectToDelete) {
-        execute({ Session session ->
-            session.delete(objectToDelete)
-        } as SessionCallback)
+    @Override
+    D insert(D instance) {
+        registry.findInstanceApi(persistentClass, null).insert(instance)
     }
 
-    /**
-     * Deletes a list of objects in one go and flushes when param is set
-     * @param objectsToDelete Collection of objects to delete
-     */
-    void deleteAll(Map params, Iterable objectToDelete) {
-        execute({ Session session ->
-            session.delete(objectToDelete)
-            if (params?.flush) {
-                session.flush()
-            }
-        } as SessionCallback)
+    @Override
+    D insert(D instance, Map params) {
+        registry.findInstanceApi(persistentClass, null).insert(instance, params)
     }
 
-    /**
-     * Creates an instance of this class
-     * @return The created instance
-     */
-    @CompileStatic(TypeCheckingMode.SKIP)
-    D create() {
-        D d = persistentClass.newInstance()
+    @Override
+    D merge(D instance) {
+        registry.findInstanceApi(persistentClass, null).merge(instance)
+    }
 
-        def applicationContext = datastore.applicationContext
+    @Override
+    D merge(D instance, Map params) {
+        registry.findInstanceApi(persistentClass, null).merge(instance, params)
+    }
 
-        if (applicationContext != null) {
-            applicationContext.autowireCapableBeanFactory.autowireBeanProperties(
-                    d, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
-        }
+    @Override
+    D save(D instance, boolean validate) {
+        registry.findInstanceApi(persistentClass, null).save(instance, validate)
+    }
 
-        return d
+    @Override
+    D save(D instance, Map params) {
+        registry.findInstanceApi(persistentClass, null).save(instance, params)
     }
 
-    /**
-     * Retrieves an object from the datastore. eg. Book.get(1)
-     */
+    @Override
+    Serializable ident(D instance) {
+        registry.findInstanceApi(persistentClass, null).ident(instance)
+    }
+
+    @Override
+    D attach(D instance) {
+        registry.findInstanceApi(persistentClass, null).attach(instance)
+    }
+
+    @Override
+    boolean isAttached(D instance) {
+        registry.findInstanceApi(persistentClass, null).isAttached(instance)
+    }
+
+    @Override
+    void discard(D instance) {
+        registry.findInstanceApi(persistentClass, null).discard(instance)
+    }
+
+    @Override
+    void delete(D instance) {
+        registry.findInstanceApi(persistentClass, null).delete(instance)
+    }
+
+    @Override
+    void delete(D instance, Map params) {
+        registry.findInstanceApi(persistentClass, null).delete(instance, params)
+    }
+
+    // GormStaticOperations
+    @Override
     D get(Serializable id) {
-        (D) execute({ Session session ->
-            session.retrieve((Class)persistentClass, id)
-        } as SessionCallback)
+        execute({ Session session ->
+            session.retrieve(persistentClass, id)
+        } as SessionCallback<D>)
     }
 
-    /**
-     * Retrieves an object from the datastore. eg. Book.read(1)
-     *
-     * Since the datastore abstraction doesn't support dirty checking yet this
-     * just delegates to {@link #get(Serializable)}
-     */
+    @Override
     D read(Serializable id) {
-        (D) execute({ Session session ->
-            session.retrieve((Class)persistentClass, id)
-        } as SessionCallback)
+        get(id)
     }
 
-    /**
-     * Retrieves an object from the datastore as a proxy. eg. Book.load(1)
-     */
+    @Override
     D load(Serializable id) {
-        (D) execute({ Session session ->
-            session.proxy((Class)persistentClass, id)
-        } as SessionCallback)
+        execute({ Session session ->
+            session.proxy(persistentClass, id)
+        } as SessionCallback<D>)
     }
 
-    /**
-     * Retrieves an object from the datastore as a proxy. eg. Book.proxy(1)
-     */
+    @Override
     D proxy(Serializable id) {
         load(id)
     }
 
-    /**
-     * Retrieve all the objects for the given identifiers
-     * @param ids The identifiers to operate against
-     * @return A list of identifiers
-     */
-    List<D> getAll(Iterable<Serializable> ids) {
-        return getAll(ids as Serializable[])
+    @Override
+    List<D> getAll(Serializable... ids) {
+        execute({ Session session ->
+            session.retrieveAll(persistentClass, ids)
+        } as SessionCallback<List<D>>)
     }
 
-    /**
-     * Retrieve all the objects for the given identifiers
-     * @param ids The identifiers to operate against
-     * @return A list of identifiers
-     */
-    List<D> getAll(Serializable... ids) {
-        (List<D>) execute({ Session session ->
-            session.retrieveAll(persistentClass, ids.flatten())
-        } as SessionCallback)
+    @Override
+    List<D> getAll(Iterable<Serializable> ids) {
+        execute({ Session session ->
+            session.retrieveAll(persistentClass, ids)
+        } as SessionCallback<List<D>>)
     }
 
-    /**
-     * @return Synonym for {@link #list()}
-     */
+    @Override
     List<D> getAll() {
         list()
     }
 
-    /**
-     * Creates a criteria builder instance
-     */
-    BuildableCriteria createCriteria() {
-        new CriteriaBuilder(persistentClass, datastore.currentSession)
+    @Override
+    List<D> list() {
+        list(Collections.emptyMap())
     }
 
-    /**
-     * Creates a criteria builder instance
-     */
-    def withCriteria(@DelegatesTo(Criteria) Closure callable) {
+    @Override
+    List<D> list(Map params) {
         execute({ Session session ->
-            InvokerHelper.invokeMethod(createCriteria(), 'call', callable)
-        } as SessionCallback)
-    }
-
-    /**
-     * Creates a criteria builder instance
-     */
-    def withCriteria(Map builderArgs, @DelegatesTo(Criteria) Closure callable) {
-        def criteriaBuilder = createCriteria()
-        def builderBean = PropertyAccessorFactory.forBeanPropertyAccess(criteriaBuilder)
-        for (entry in builderArgs.entrySet()) {
-            String propertyName = entry.key.toString()
-            if (builderBean.isWritableProperty(propertyName)) {
-                builderBean.setPropertyValue(propertyName, entry.value)
+            org.grails.datastore.mapping.query.Query q = session.createQuery(persistentClass)
+            org.grails.datastore.gorm.finders.DynamicFinder.populateArgumentsForCriteria(persistentClass, q, params)
+            if (params?.containsKey('max')) {
+                return new grails.gorm.PagedResultList(q)
             }
-        }
+            q.list()
+        } as SessionCallback<List<D>>)
+    }
 
-        if (builderArgs?.uniqueResult) {
-            execute({ Session session ->
-                InvokerHelper.invokeMethod(criteriaBuilder, 'get', callable)
-            } as SessionCallback)
+    @Override
+    Integer count() {
+        log.debug('GormStaticApi.count() called for {}', persistentClass.name)
+        Integer result = execute({ Session session ->
+            def query = session.createQuery(persistentClass)
+            query.projections().count()
+            def res = query.singleResult()
+            log.debug('Query singleResult returned {}', res)
+            res instanceof Number ? ((Number)res).intValue() : 0
+        } as SessionCallback<Integer>)
+        log.debug('count() result is {}', result)
+        return result
+    }
 
-        }
-        else {
-            execute({ Session session ->
-                InvokerHelper.invokeMethod(criteriaBuilder, 'list', callable)
-            } as SessionCallback)
-        }
+    @Override
+    Integer getCount() {
+        count()
+    }
 
+    @Override
+    boolean exists(Serializable id) {
+        get(id) != null
     }
 
-    /**
-     * Locks an instance for an update
-     * @param id The identifier
-     * @return The instance
-     */
-    D lock(Serializable id) {
-        (D) execute({ Session session ->
-            session.lock((Class)persistentClass, id)
-        } as SessionCallback)
+    @Override
+    D first() {
+        first([:])
     }
 
-    /**
-     * Merges an instance with the current session
-     * @param d The object to merge
-     * @return The instance
-     */
     @Override
-    def propertyMissing(D instance, String name) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).propertyMissing(instance, name)
+    D first(String propertyName) {
+        first(sort: propertyName)
     }
 
     @Override
-    boolean instanceOf(D instance, Class cls) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).instanceOf(instance, cls)
+    D first(Map params) {
+        list(params + [max: 1])?.getAt(0)
     }
 
     @Override
-    D lock(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).lock(instance)
+    D last() {
+        last([:])
     }
 
     @Override
-    def <T> T mutex(D instance, Closure<T> callable) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).mutex(instance, callable)
+    D last(String propertyName) {
+        last(sort: propertyName, order: 'desc')
     }
 
     @Override
-    D refresh(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).refresh(instance)
+    D last(Map params) {
+        list(params + [max: 1, order: 'desc'])?.getAt(0)
     }
 
     @Override
-    D save(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).save(instance)
+    BuildableCriteria createCriteria() {
+        execute({ Session session ->
+            new CriteriaBuilder(persistentClass, session)
+        } as SessionCallback<BuildableCriteria>)
     }
 
     @Override
-    D insert(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).insert(instance)
+    def <T1> T1 withCriteria(Closure<T1> callable) {
+        createCriteria().list(callable)
     }
 
     @Override
-    D insert(D instance, Map params) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).insert(instance, params)
+    def <T1> T1 withCriteria(Map builderArgs, Closure callable) {
+        createCriteria().list(builderArgs, callable)
     }
 
-    D merge(D d) {
+    @Override
+    D lock(Serializable id) {
         execute({ Session session ->
-            session.persist(d)
-            return d
-        } as SessionCallback)
+            session.lock(persistentClass, id)
+        } as SessionCallback<D>)
     }
 
     @Override
-    D merge(D instance, Map params) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).merge(instance, params)
+    grails.gorm.DetachedCriteria<D> where(Closure callable) {
+        new grails.gorm.DetachedCriteria<D>(persistentClass).withConnection(qualifier).where(callable)
     }
 
     @Override
-    D save(D instance, boolean validate) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).save(instance, validate)
+    grails.gorm.DetachedCriteria<D> whereLazy(Closure callable) {
+        where(callable)
     }
 
     @Override
-    D save(D instance, Map params) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).save(instance, params)
+    grails.gorm.DetachedCriteria<D> whereAny(Closure callable) {
+        new grails.gorm.DetachedCriteria<D>(persistentClass).or(callable)
     }
 
     @Override
-    Serializable ident(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).ident(instance)
+    List<Serializable> saveAll(Iterable<?> objectsToSave) {
+        execute({ Session session ->
+            session.persist(objectsToSave)
+            session.flush()
+        } as SessionCallback<List<Serializable>>)
     }
 
     @Override
-    D attach(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).attach(instance)
+    List<Serializable> saveAll(Object... objectsToSave) {
+        saveAll(Arrays.asList(objectsToSave))
     }
 
     @Override
-    boolean isAttached(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).isAttached(instance)
+    Number deleteAll() {
+        execute({ Session session ->
+            session.deleteAll(new DetachedCriteria(persistentClass))
+        } as SessionCallback<Number>)
     }
 
     @Override
-    void discard(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).discard(instance)
+    Number deleteAll(Map params) {
+        deleteAll()
     }
 
     @Override
-    void delete(D instance) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).delete(instance)
+    void deleteAll(Iterable objectsToDelete) {
+        execute({ Session session ->
+            for (obj in objectsToDelete) {
+                session.delete(obj)
+            }
+        } as SessionCallback<Void>)
     }
 
     @Override
-    void delete(D instance, Map params) {
-        GormEnhancer.findInstanceApi(persistentClass, defaultQualifier).delete(instance, params)
+    void deleteAll(Object... objectsToDelete) {
+        deleteAll(Arrays.asList(objectsToDelete))
     }
-    /**
-     * Counts the number of persisted entities
-     * @return The number of persisted entities
-     */
-    Integer count() {
-        (Integer) execute({ Session session ->
 
-            def q = session.createQuery(persistentClass)
-            q.projections().count()
-            def result = q.singleResult()
-            if (!(result instanceof Number)) {
-                result = result.toString()
-            }
-            try {
-                return result as Integer
+    @Override
+    void deleteAll(Map params, Iterable objectsToDelete) {
+        execute({ Session session ->
+            for (obj in objectsToDelete) {
+                session.delete(obj)
             }
-            catch (NumberFormatException e) {
-                return 0
+            if (params?.flush) {
+                session.flush()
             }
-        } as SessionCallback)
+        } as SessionCallback<Void>)
     }
 
-    /**
-     * Same as {@link #count()} but allows property-style syntax (Foo.count)
-     */
-    Integer getCount() {
-        count()
-    }
-
-    /**
-     * Checks whether an entity exists
-     */
-    boolean exists(Serializable id) {
-        get(id) != null
+    @Override
+    void deleteAll(Map params, Object... objectsToDelete) {
+        deleteAll(params, Arrays.asList(objectsToDelete))
     }
 
-    /**
-     * Lists objects in the datastore. eg. Book.list(max:10)
-     *
-     * @param params Any parameters such as offset, max etc.
-     * @return A list of results
-     */
-    List<D> list(Map params) {
-        (List<D>) execute({ Session session ->
-            Query q = session.createQuery(persistentClass)
-            DynamicFinder.populateArgumentsForCriteria(persistentClass, q, params)
-            if (params?.max) {
-                return new PagedResultList(q)
-            }
-            return q.list()
-        } as SessionCallback)
+    @Override
+    D create() {
+        persistentClass.newInstance()
     }
 
-    /**
-     * List all entities
-     *
-     * @return The list of all entities
-     */
-    List<D> list() {
-        (List<D>) execute({ Session session ->
-            session.createQuery(persistentClass).list()
-        } as SessionCallback)
+    @Override
+    List<D> findAll() {
+        list()
     }
 
-    /**
-     * The same as {@link #list()}
-     *
-     * @return The list of all entities
-     */
-    List<D> findAll(Map params = Collections.emptyMap()) {
+    @Override
+    List<D> findAll(Map params) {
         list(params)
     }
 
-    /**
-     * Finds an object by example
-     *
-     * @param example The example
-     * @return A list of matching results
-     */
+    @Override
     List<D> findAll(D example) {
         findAll(example, Collections.emptyMap())
     }
 
-    /**
-     * Finds an object by example using the given arguments for pagination
-     *
-     * @param example The example
-     * @param args The arguments
-     *
-     * @return A list of matching results
-     */
+    @Override
     List<D> findAll(D example, Map args) {
-        if (!persistentEntity.isInstance(example)) {
-            return Collections.emptyList()
-        }
+        execute({ Session session ->
+            def query = session.createQuery(persistentClass)
+            populateQueryByExample(session, query, example)
+            query.list(args)
+        } as SessionCallback<List<D>>)
+    }
 
-        def queryMap = createQueryMapForExample(persistentEntity, example)
-        return findAllWhere(queryMap, args)
+    @Override
+    List<D> findAll(Closure callable) {
+        where(callable).list()
     }
 
-    /**
-     * Finds the first object using the natural sort order
-     *
-     * @return the first object in the datastore, null if none exist
-     */
-    D first() {
-        first([:])
+    @Override
+    List<D> findAll(Map args, Closure callable) {
+        where(callable).list(args)
     }
 
-    /**
-     * Finds the first object sorted by propertyName
-     *
-     * @param propertyName the name of the property to sort by
-     *
-     * @return the first object in the datastore sorted by propertyName, null if none exist
-     */
-    D first(String propertyName) {
-        first(sort: propertyName)
+    @Override
+    D find(D example) {
+        find(example, Collections.emptyMap())
     }
 
-    /**
-     * Finds the first object.  If queryParams includes 'sort', that will
-     * dictate the sort order, otherwise natural sort order will be used.
-     * queryParams may include any of the same parameters that might be passed
-     * to the list(Map) method.  This method will ignore 'order' and 'max' as
-     * those are always 'asc' and 1, respectively.
-     *
-     * @return the first object in the datastore, null if none exist
-     */
-    D first(Map queryParams) {
-        queryParams.max = 1
-        queryParams.order = 'asc'
-        if (!queryParams.containsKey('sort')) {
-            def idPropertyName = persistentEntity.identity?.name
-            if (idPropertyName) {
-                queryParams.sort = idPropertyName
+    @Override
+    D find(D example, Map args) {
+        execute({ Session session ->
+            def query = session.createQuery(persistentClass)
+            populateQueryByExample(session, query, example)
+            query.singleResult()
+        } as SessionCallback<D>)
+    }
+
+    protected void populateQueryByExample(Session session, org.grails.datastore.mapping.query.Query query, D example) {
+        def pe = getGormPersistentEntity()
+        def persister = session.getPersister(example)
+        if (persister != null) {
+            def id = persister.getObjectIdentifier(example)
+            if (id != null) {
+                query.add(org.grails.datastore.mapping.query.Restrictions.eq(pe.identity.name, id))
+            }
+            else {
+                def ea = pe.mappingContext.createEntityAccess(pe, example)
+                for (prop in pe.persistentProperties) {
+                    if (prop instanceof org.grails.datastore.mapping.model.types.Simple || prop instanceof org.grails.datastore.mapping.model.types.Basic) {
+                        def val = ea.getProperty(prop.name)
+                        if (val != null) {
+                            query.add(org.grails.datastore.mapping.query.Restrictions.eq(prop.name, val))
+                        }
+                    }
+                }
             }
         }
-        def resultList = list(queryParams)
-        resultList ? resultList[0] : null
     }
 
-    /**
-     * Finds the last object using the natural sort order
-     *
-     * @return the last object in the datastore, null if none exist
-     */
-    D last() {
-        last([:])
+    @Override
+    D find(Closure callable) {
+        where(callable).find()
     }
 
-    /**
-     * Finds the last object sorted by propertyName
-     *
-     * @param propertyName the name of the property to sort by
-     *
-     * @return the last object in the datastore sorted by propertyName, null if none exist
-     */
-    D last(String propertyName) {
-        last(sort: propertyName)
+    @Override
+    D findWhere(Map queryMap) {
+        findWhere(queryMap, [:])
     }
 
-/**
- * Finds the last object.  If queryParams includes 'sort', that will
- * dictate the sort order, otherwise natural sort order will be used.
- * queryParams may include any of the same parameters that might be passed
- * to the list(Map) method.  This method will ignore 'order' and 'max' as
- * those are always 'asc' and 1, respectively.
- *
- * @return the last object in the datastore, null if none exist
- */
-    D last(Map queryParams) {
-        queryParams.max = 1
-        queryParams.order = 'desc'
-        if (!queryParams.containsKey('sort')) {
-            def idPropertyName = persistentEntity.identity?.name
-            if (idPropertyName) {
-                queryParams.sort = idPropertyName
+    @Override
+    D findWhere(Map queryMap, Map args) {
+        where {
+            for (entry in queryMap) {
+                eq(entry.key.toString(), entry.value)
             }
-        }
-        def resultList = list(queryParams)
-        resultList ? resultList[0] : null
+        }.find(args)
     }
 
-    /**
-     * Finds all results matching all of the given conditions. Eg. Book.findAllWhere(author:"Stephen King", title:"The Stand")
-     *
-     * @param queryMap The map of conditions
-     * @return A list of results
-     */
+    @Override
     List<D> findAllWhere(Map queryMap) {
-        findAllWhere(queryMap, Collections.emptyMap())
+        findAllWhere(queryMap, [:])
     }
 
-    /**
-     * Finds all results matching all of the given conditions. Eg. Book.findAllWhere(author:"Stephen King", title:"The Stand")
-     *
-     * @param queryMap The map of conditions
-     * @param args The Query arguments
-     *
-     * @return A list of results
-     */
+    @Override
     List<D> findAllWhere(Map queryMap, Map args) {
-        (List<D>) execute({ Session session ->
-            Query q = session.createQuery(persistentClass)
-
-            Map<String, Object> processedQueryMap = [:]
-            queryMap.each { key, value -> processedQueryMap[key.toString()] = value }
-            q.allEq(processedQueryMap)
-
-            DynamicFinder.populateArgumentsForCriteria(persistentClass, q, args)
-            q.list()
-        } as SessionCallback<List>)
-    }
-
-    /**
-     * Finds an object by example
-     *
-     * @param example The example
-     * @return A list of matching results
-     */
-    D find(D example) {
-        find(example, Collections.emptyMap())
-    }
-
-    /**
-     * Finds an object by example using the given arguments for pagination
-     *
-     * @param example The example
-     * @param args The arguments
-     *
-     * @return A list of matching results
-     */
-    D find(D example, Map args) {
-        if (persistentEntity.isInstance(example)) {
-            def queryMap = createQueryMapForExample(persistentEntity, example)
-            return findWhere(queryMap, args)
-        }
-        return null
-    }
-
-    /**
-     * Finds a single result matching all of the given conditions. Eg. Book.findWhere(author:"Stephen King", title:"The Stand")
-     *
-     * @param queryMap The map of conditions
-     * @return A single result
-     */
-    D findWhere(Map queryMap) {
-        findWhere(queryMap, Collections.emptyMap())
+        where {
+            for (entry in queryMap) {
+                eq(entry.key.toString(), entry.value)
+            }
+        }.list(args)
     }
 
-    /**
-     * Finds a single result matching all of the given conditions. Eg. Book.findWhere(author:"Stephen King", title:"The Stand")
-     *
-     * @param queryMap The map of conditions
-     * @param args The Query arguments
-     *
-     * @return A single result
-     */
-    D findWhere(Map queryMap, Map args) {
-        execute({ Session session ->
-            Query q = session.createQuery(persistentClass)
-            if (queryMap) {
-                Map<String, Object> processedQueryMap = [:]
-                queryMap.each { key, value -> processedQueryMap[key.toString()] = value }
-                q.allEq(processedQueryMap)
-            }
-            DynamicFinder.populateArgumentsForCriteria(persistentClass, q, args)
-            q.singleResult()
-        } as SessionCallback)
-    }
-
-    /**
-     * Finds a single result matching all of the given conditions. Eg. Book.findWhere(author:"Stephen King", title:"The Stand").  If
-     * a matching persistent entity is not found a new entity is created and returned.
-     *
-     * @param queryMap The map of conditions
-     * @return A single result
-     */
+    @Override
     D findOrCreateWhere(Map queryMap) {
-        internalFindOrCreate(queryMap, false)
+        D instance = findWhere(queryMap)
+        if (instance == null) {
+            instance = persistentClass.newInstance(queryMap)
+        }
+        return instance
     }
 
-    /**
-     * Finds a single result matching all of the given conditions. Eg. Book.findWhere(author:"Stephen King", title:"The Stand").  If
-     * a matching persistent entity is not found a new entity is created, saved and returned.
-     *
-     * @param queryMap The map of conditions
-     * @return A single result
-     */
+    @Override
     D findOrSaveWhere(Map queryMap) {
-        internalFindOrCreate(queryMap, true)
+        D instance = findWhere(queryMap)
+        if (instance == null) {
+            instance = persistentClass.newInstance(queryMap)
+            ((GormEntity<D>)instance).save(flush: true)
+        }
+        return instance
     }
 
-    /**
-     * Execute a closure whose first argument is a reference to the current session.
-     *
-     * @param callable the closure
-     * @return The result of the closure
-     */
-    <T> T  withSession(Closure<T> callable) {
+    @Override
+    def <T1> T1 withSession(Closure<T1> callable) {
         execute({ Session session ->
             callable.call(session)
-        } as SessionCallback)
+        } as SessionCallback<T1>)
     }
 
-    /**
-     * Same as withSession, but present for the case where withSession is overridden to use the Hibernate session
-     *
-     * @param callable the closure
-     * @return The result of the closure
-     */
-    <T> T  withDatastoreSession(Closure<T> callable) {
-        execute({ Session session ->
-            callable.call(session)
-        } as SessionCallback)
+    @Override
+    def <T1> T1 withDatastoreSession(Closure<T1> callable) {
+        withSession(callable)
     }
 
-    /**
-     * Executes the closure within the context of a transaction, creating one if none is present or joining
-     * an existing transaction if one is already present.
-     *
-     * @param callable The closure to call
-     * @return The result of the closure execution
-     * @see #withTransaction(Map, Closure)
-     * @see #withNewTransaction(Closure)
-     * @see #withNewTransaction(Map, Closure)
-     */
-    <T> T withTransaction(Closure<T> callable) {
-        withTransaction(new DefaultTransactionDefinition(), callable)
+    @Override
+    def <T1> T1 withTransaction(Closure<T1> callable) {
+        createTransactionTemplate().execute(callable)
     }
 
     @Override
-    def <T> T withTenant(Serializable tenantId, Closure<T> callable) {
-        if (multiTenancyMode == MultiTenancyMode.DATABASE) {
-            Tenants.withId((Class<Datastore>) GormEnhancer.findDatastore(persistentClass, tenantId.toString()).getClass(), tenantId, callable)
-        }
-        else if (multiTenancyMode.isSharedConnection()) {
-            Tenants.withId((Class<Datastore>) GormEnhancer.findDatastore(persistentClass, ConnectionSource.DEFAULT).getClass(), tenantId, callable)
-        }
-        else {
-            throw new UnsupportedOperationException("Method not supported in multi tenancy mode $multiTenancyMode")
-        }
+    def <T1> T1 withNewTransaction(Closure<T1> callable) {
+        DefaultTransactionDefinition definition = new DefaultTransactionDefinition()
+        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
+        withTransaction(definition, callable)
     }
 
     @Override
-    GormAllOperations<D> eachTenant(Closure callable) {
-        if (multiTenancyMode != MultiTenancyMode.NONE) {
-            Tenants.eachTenant(callable)
-            return this
-        }
-        else {
-            throw new UnsupportedOperationException("Method not supported in multi tenancy mode $multiTenancyMode")
+    def <T1> T1 withTransaction(Map transactionProperties, Closure<T1> callable) {
+        DefaultTransactionDefinition definition = new DefaultTransactionDefinition()
+        transactionProperties.each { k, v ->
+            if (v instanceof CharSequence && !(v instanceof String)) {
+                v = v.toString()
+            }
+            try {
+                definition[k as String] = v
+            } catch (MissingPropertyException mpe) {
+                throw new IllegalArgumentException("[${k}] is not a valid transaction property.")
+            }
         }
+        withTransaction(definition, callable)
     }
 
     @Override
-    GormAllOperations<D> withTenant(Serializable tenantId) {
-        if (multiTenancyMode == MultiTenancyMode.DATABASE) {
-            return GormEnhancer.findStaticApi(persistentClass, tenantId.toString())
-        }
-        else if (multiTenancyMode.isSharedConnection()) {
-            def staticApi = GormEnhancer.findStaticApi(persistentClass, ConnectionSource.DEFAULT)
-            return new TenantDelegatingGormOperations<D>(datastore, tenantId, staticApi)
-        }
-        else {
-            throw new UnsupportedOperationException("Method not supported in multi tenancy mode $multiTenancyMode")
-        }
-    }
-    /**
-     * Executes the closure within the context of a new transaction
-     *
-     * @param callable The closure to call
-     * @return The result of the closure execution
-     * @see #withTransaction(Closure)
-     * @see #withTransaction(Map, Closure)
-     * @see #withNewTransaction(Map, Closure)
-     */
-    <T> T  withNewTransaction(Closure<T> callable) {
-        withTransaction([propagationBehavior: TransactionDefinition.PROPAGATION_REQUIRES_NEW], callable)
-    }
-
-    /**
-     * Executes the closure within the context of a transaction which is
-     * configured with the properties contained in transactionProperties.
-     * transactionProperties may contain any properties supported by
-     * {@link DefaultTransactionDefinition}.
-     *
-     * <blockquote>
-     * <pre>
-     * SomeEntity.withTransaction([propagationBehavior: TransactionDefinition.PROPAGATION_REQUIRES_NEW,
-     *                             isolationLevel: TransactionDefinition.ISOLATION_REPEATABLE_READ]) {
-     *     // ...
-     * }
-     * </pre>
-     * </blockquote>
-     *
-     * @param transactionProperties properties to configure the transaction properties
-     * @param callable The closure to call
-     * @return The result of the closure execution
-     * @see DefaultTransactionDefinition
-     * @see #withNewTransaction(Closure)
-     * @see #withNewTransaction(Map, Closure)
-     * @see #withTransaction(Closure)
-     */
-    <T> T  withTransaction(Map transactionProperties, Closure<T> callable) {
-        def transactionDefinition = new DefaultTransactionDefinition()
+    def <T1> T1 withNewTransaction(Map transactionProperties, Closure<T1> callable) {
+        DefaultTransactionDefinition definition = new DefaultTransactionDefinition()
         transactionProperties.each { k, v ->
             if (v instanceof CharSequence && !(v instanceof String)) {
                 v = v.toString()
             }
             try {
-                transactionDefinition[k as String] = v
+                definition[k as String] = v
             } catch (MissingPropertyException mpe) {
                 throw new IllegalArgumentException("[${k}] is not a valid transaction property.")
             }
         }
+        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
+        withTransaction(definition, callable)
+    }
 
-        withTransaction(transactionDefinition, callable)
-    }
-
-    /**
-     * Executes the closure within the context of a new transaction which is
-     * configured with the properties contained in transactionProperties.
-     * transactionProperties may contain any properties supported by
-     * {@link DefaultTransactionDefinition}.  Note that if transactionProperties
-     * includes entries for propagationBehavior or propagationName, those values
-     * will be ignored.  This method always sets the propagation level to
-     * TransactionDefinition.REQUIRES_NEW.
-     *
-     * <blockquote>
-     * <pre>
-     * SomeEntity.withNewTransaction([isolationLevel: TransactionDefinition.ISOLATION_REPEATABLE_READ]) {
-     *     // ...
-     * }
-     * </pre>
-     * </blockquote>
-     *
-     * @param transactionProperties properties to configure the transaction properties
-     * @param callable The closure to call
-     * @return The result of the closure execution
-     * @see DefaultTransactionDefinition
-     * @see #withNewTransaction(Closure)
-     * @see #withTransaction(Closure)
-     * @see #withTransaction(Map, Closure)
-     */
-    <T> T withNewTransaction(Map transactionProperties, Closure<T> callable) {
-        def props = new HashMap(transactionProperties)
-        props.remove('propagationName')
-        props.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
-        withTransaction(props, callable)
-    }
-
-    /**
-     * Executes the closure within the context of a transaction for the given {@link TransactionDefinition}
-     *
-     * @param callable The closure to call
-     * @return The result of the closure execution
-     */
-    <T> T withTransaction(TransactionDefinition definition, Closure<T> callable) {
-        Assert.notNull(transactionManager, 'No transactionManager bean configured')
-
-        if (!callable) {
-            return
-        }
+    @Override
+    def <T1> T1 withTransaction(org.springframework.transaction.TransactionDefinition definition, Closure<T1> callable) {
+        createTransactionTemplate(definition).execute(callable)
+    }
 
-        new GrailsTransactionTemplate(transactionManager, definition).execute(callable)
+    protected GrailsTransactionTemplate createTransactionTemplate() {
+        getTransactionTemplateFactory().createTransactionTemplate(getTransactionManager())
     }
 
-    /**
-     * Creates and binds a new session for the scope of the given closure
-     */
-    <T> T withNewSession(Closure<T> callable) {
-        def session = datastore.connect()
-        try {
-            DatastoreUtils.bindNewSession(session)
-            return callable?.call(session)
-        }
-        finally {
-            DatastoreUtils.unbindSession(session)
-        }
+    protected GrailsTransactionTemplate createTransactionTemplate(org.springframework.transaction.TransactionDefinition definition) {
+        getTransactionTemplateFactory().createTransactionTemplate(getTransactionManager(), definition)
     }
 
-    /**
-     * Creates and binds a new session for the scope of the given closure
-     */
-    <T> T  withStatelessSession(Closure<T> callable) {
-        if (datastore instanceof StatelessDatastore) {
-            def session = datastore.connectStateless()
-            try {
-                DatastoreUtils.bindNewSession(session)
-                return callable?.call(session)
-            }
-            finally {
-                DatastoreUtils.unbindSession(session)
-            }
-        }
-        else {
-            throw new UnsupportedOperationException('Stateless sessions not supported by implementation')
-        }
+    protected TransactionTemplateFactory getTransactionTemplateFactory() {
+        DEFAULT_TRANSACTION_TEMPLATE_FACTORY
+    }
+
+    @Override
+    def <T1> T1 withNewSession(Closure<T1> callable) {
+        Datastore ds = getDatastore()
+        DatastoreUtils.executeWithNewSession(ds, { Session session ->
+            callable.call(session)
+        } as SessionCallback<T1>)
+    }
+
+    @Override
+    def <T1> T1 withStatelessSession(Closure<T1> callable) {
+        Datastore ds = getDatastore()
+        DatastoreUtils.executeWithNewSession(ds, { Session session ->
+            callable.call(session)
+        } as SessionCallback<T1>)
     }
 
     @Override
     List executeQuery(CharSequence query) {
-        executeQuery(query, Collections.emptyMap(), Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List executeQuery(CharSequence query, Map args) {
-        executeQuery(query, args, args)
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List executeQuery(CharSequence query, Map params, Map args) {
-        unsupported('executeQuery')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List executeQuery(CharSequence query, Collection params) {
-        executeQuery(query, params, Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
-    List executeQuery(CharSequence query, Object...params) {
-        executeQuery(query, params.toList(), Collections.emptyMap())
+    List executeQuery(CharSequence query, Object... params) {
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List executeQuery(CharSequence query, Collection params, Map args) {
-        unsupported('executeQuery')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [executeQuery] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     Integer executeUpdate(CharSequence query) {
-        executeUpdate(query, Collections.emptyMap(), Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     Integer executeUpdate(CharSequence query, Map args) {
-        executeUpdate(query, args, args)
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     Integer executeUpdate(CharSequence query, Map params, Map args) {
-        unsupported('executeUpdate')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     Integer executeUpdate(CharSequence query, Collection params) {
-        executeUpdate(query, params, Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
-    Integer executeUpdate(CharSequence query, Object...params) {
-        executeUpdate(query, params.toList(), Collections.emptyMap())
+    Integer executeUpdate(CharSequence query, Object... params) {
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     Integer executeUpdate(CharSequence query, Collection params, Map args) {
-        unsupported('executeUpdate')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [executeUpdate] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query) {
-        find(query, Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query, Map params) {
-        find(query, params, params)
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query, Map params, Map args) {
-        unsupported('find')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query, Collection params) {
-        find(query, params, Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query, Object[] params) {
-        find(query, params.toList(), Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     D find(CharSequence query, Collection params, Map args) {
-        unsupported('find')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [find] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query) {
-        findAll(query, Collections.emptyMap(), Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query, Map params) {
-        findAll(query, params, params)
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query, Map params, Map args) {
-        unsupported('findAll')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query, Collection params) {
-        findAll(query, params, Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query, Object[] params) {
-        findAll(query, params.toList(), Collections.emptyMap())
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
     @Override
     List<D> findAll(CharSequence query, Collection params, Map args) {
-        unsupported('findAll')
-        return null
+        throw new UnsupportedOperationException('String-based queries like [findAll] are currently not supported in this implementation of GORM. Use criteria instead.')
     }
 
-    protected void unsupported(method) {
-        throw new UnsupportedOperationException("String-based queries like [$method] are currently not supported in this implementation of GORM. Use criteria instead.")
+    @Override
+    grails.gorm.api.GormAllOperations<D> withTenant(Serializable tenantId) {
+        return (grails.gorm.api.GormAllOperations<D>) forQualifier(tenantId.toString())
     }
 
-    private Map createQueryMapForExample(PersistentEntity persistentEntity, D example) {
-        def props = persistentEntity.persistentProperties.findAll { PersistentProperty prop ->
-            !(prop instanceof Association)
+    @Override
+    def <T1> T1 withTenant(Serializable tenantId, Closure<T1> callable) {
+        withId(tenantId, callable)
+    }
+
+    @Override
+    grails.gorm.api.GormAllOperations<D> eachTenant(Closure callable) {
+        Datastore ds = registry.getDatastore(persistentClass.name, ConnectionSource.DEFAULT)
+        if (ds instanceof MultiTenantCapableDatastore) {
+            Tenants.eachTenant((MultiTenantCapableDatastore) ds, callable)
+            return this
         }
+        throw new UnsupportedOperationException("eachTenant not supported for datastore: ${ds?.class?.simpleName}")
+    }
 
-        def queryMap = [:]
-        for (PersistentProperty prop in props) {
-            def val = example[prop.name]
-            if (val != null) {
-                queryMap[prop.name] = val
-            }
+    def <T1> T1 withTenantTransaction(Serializable tenantId, Closure<T1> callable) {
+        withId(tenantId, callable)
+    }
+
+    def <T1> T1 withTenantTransaction(Serializable tenantId, org.springframework.transaction.TransactionDefinition definition, Closure<T1> callable) {
+        withId(tenantId, callable)
+    }
+
+    def <T1> T1 withId(Serializable tenantId, Closure<T1> callable) {
+        // For multi-tenancy, always resolve via the DEFAULT (root/parent) datastore.
+        // Resolving the tenant-specific datastore and then calling withNewSession() on it
+        // would fail because child datastores have empty datastoresByConnectionSource maps.
+        Datastore defaultDs = registry.getDatastore(persistentClass.name, ConnectionSource.DEFAULT)
+        if (defaultDs instanceof MultiTenantCapableDatastore) {
+            return (T1) Tenants.withId((MultiTenantCapableDatastore) defaultDs, tenantId, callable)
         }
-        return queryMap
+        // Non-multi-tenant path: resolve the specific datastore for this connection/tenant key
+        Datastore tenantDatastore = registry.apiResolver.findDatastore(persistentClass, tenantId.toString())
+        return DatastoreUtils.execute(tenantDatastore, (Session session) -> {
+            return (T1) callable.call(session)
+        } as SessionCallback<T1>)
     }
 
-    private D internalFindOrCreate(Map queryMap, boolean shouldSave) {
-        D result = findWhere(queryMap)
-        if (!result) {
-            def persistentMetaClass = GroovySystem.metaClassRegistry.getMetaClass(persistentClass)
-            result = (D) persistentMetaClass.invokeConstructor(queryMap)
-            if (shouldSave) {
-                InvokerHelper.invokeMethod(result, 'save', null)
-            }
+    def <T1> T1 withoutId(Closure<T1> callable) {
+        withId(ConnectionSource.DEFAULT, callable)
+    }
+
+    def <T1> T1 withNewSession(Serializable tenantId, Closure<T1> callable) {
+        DatastoreResolver resolver = new DatastoreResolver() {
+            @Override Datastore resolve() { registry.apiResolver.findDatastore(persistentClass, tenantId.toString()) }
         }
-        result
+        Datastore tenantDatastore = resolver.resolve()
+        DatastoreUtils.executeWithNewSession(tenantDatastore, { Session session ->
+            return (T1) callable.call(session)
+        } as SessionCallback<T1>)
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApiRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApiRegistry.groovy
new file mode 100644
index 0000000000..3e7c541671
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApiRegistry.groovy
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.model.MappingContext
+
+@CompileStatic
+class GormStaticApiRegistry extends AbstractGormApiRegistry<GormStaticApi> {
+
+    GormStaticApiRegistry(GormRegistry registry) {
+        super(registry)
+    }
+
+    @Override
+    protected GormStaticApi qualify(GormStaticApi api, String qualifier) {
+        Class persistentClass = api.persistentClass
+        Datastore datastore = registry.apiResolver.findDatastore(persistentClass, qualifier)
+        if (datastore == null) {
+            return api
+        }
+        MappingContext mappingContext = datastore.mappingContext
+        DatastoreResolver resolver = registry.createClassDatastoreResolver(persistentClass, qualifier)
+        return registry.getApiFactory(datastore).createStaticApi(persistentClass, mappingContext, resolver, qualifier, registry)
+    }
+
+    <D> GormStaticApi<D> findStaticApi(Class<D> entity, String qualifier = null) {
+        String className = className(entity)
+        GormStaticApi api = get(className)
+        if (api == null) {
+            throw stateException(entity)
+        }
+
+        if (qualifier != null && qualifier != ConnectionSource.DEFAULT) {
+            return api.forQualifier(qualifier)
+        }
+        return (GormStaticApi<D>) api
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidateable.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidateable.groovy
index 6b6be5e917..71cf832584 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidateable.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidateable.groovy
@@ -138,6 +138,10 @@ trait GormValidateable {
      */
     @Generated
     private GormValidationApi currentGormValidationApi() {
-        GormEnhancer.findValidationApi(getClass())
+        GormValidationApi api = GormRegistry.instance.findValidationApi(getClass())
+        if (api == null) {
+            throw new IllegalStateException("No GORM implementation configured for class [${getClass().name}]. Ensure GORM has been initialized correctly")
+        }
+        api
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApi.groovy
index 2f039069a8..dab1799d9b 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApi.groovy
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
 import jakarta.persistence.FlushModeType
 
 import org.springframework.context.ApplicationEventPublisher
+import org.springframework.transaction.PlatformTransactionManager
 import org.springframework.validation.Errors
 import org.springframework.validation.FieldError
 import org.springframework.validation.ObjectError
@@ -32,11 +33,15 @@ import grails.gorm.validation.CascadingValidator
 import org.grails.datastore.gorm.support.BeforeValidateHelper
 import org.grails.datastore.gorm.validation.ValidatorProvider
 import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.SessionCallback
 import org.grails.datastore.mapping.engine.event.ValidationEvent
 import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.datastore.mapping.model.config.GormProperties
 import org.grails.datastore.mapping.reflect.ClassUtils
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 import org.grails.datastore.mapping.validation.ValidationErrors
 
 /**
@@ -58,31 +63,79 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
     protected final boolean hasDatastore
 
     GormValidationApi(Class<D> persistentClass, Datastore datastore) {
-        super(persistentClass, datastore)
+        this(persistentClass, datastore, (GormRegistry) null)
+    }
+
+    GormValidationApi(Class<D> persistentClass, Datastore datastore, GormRegistry registry) {
+        super(persistentClass, datastore, registry)
         beforeValidateHelper = new BeforeValidateHelper()
-        this.mappingContext = datastore.mappingContext
-        this.eventPublisher = datastore.applicationEventPublisher
+        this.mappingContext = datastore?.mappingContext
+        this.eventPublisher = datastore?.applicationEventPublisher
         this.hasDatastore = datastore != null
     }
 
+    GormValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver) {
+        this(persistentClass, mappingContext, datastoreResolver, (GormRegistry) null)
+    }
+
+    GormValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver, GormRegistry registry) {
+        super(persistentClass, mappingContext, datastoreResolver, null, registry)
+        beforeValidateHelper = new BeforeValidateHelper()
+        this.mappingContext = mappingContext
+        this.eventPublisher = null // Will be resolved if needed
+        this.hasDatastore = true
+    }
+
+    GormValidationApi<D> forQualifier(String qualifier) {
+        if (!hasDatastore) return this
+        DatastoreResolver resolver = new DatastoreResolver() {
+            @Override Datastore resolve() { registry.apiResolver.findDatastore(persistentClass, qualifier) }
+        }
+        return new GormValidationApi<D>(persistentClass, mappingContext, resolver, registry)
+    }
+
     GormValidationApi(Class<D> persistentClass, MappingContext mappingContext, ApplicationEventPublisher eventPublisher) {
-        super(persistentClass, mappingContext)
+        super(persistentClass, mappingContext, null)
         beforeValidateHelper = new BeforeValidateHelper()
         this.mappingContext = mappingContext
         this.eventPublisher = eventPublisher
         this.hasDatastore = false
     }
 
+    @Override
+    protected <T1> T1 executeQualified(String qualifier, SessionCallback<T1> callback) {
+        GormValidationApi<D> qualifiedApi = registry.findValidationApi(persistentClass, qualifier)
+        if (qualifiedApi != null && qualifiedApi != this) {
+            return (T1) qualifiedApi.execute(callback)
+        }
+        return DatastoreUtils.execute(getDatastore(), callback)
+    }
+
+    @Override
+    PlatformTransactionManager getTransactionManager() {
+        Datastore ds = getDatastore()
+        if (ds instanceof TransactionCapableDatastore) {
+            return ((TransactionCapableDatastore) ds).getTransactionManager()
+        }
+        return null
+    }
+
     Validator getValidator() {
-        if (!internalValidator) {
-            if (persistentEntity instanceof ValidatorProvider) {
-                internalValidator = ((ValidatorProvider) persistentEntity).validator
-            }
-            if (!internalValidator) {
-                internalValidator = mappingContext.getEntityValidator(persistentEntity)
+        if (internalValidator) {
+            return internalValidator
+        }
+        Validator validator = null
+        PersistentEntity persistentEntity = getGormPersistentEntity()
+        if (persistentEntity instanceof ValidatorProvider) {
+            validator = ((ValidatorProvider) persistentEntity).validator
+        }
+        if (!validator) {
+            MappingContext currentMappingContext = getDatastore()?.getMappingContext() ?: this.mappingContext
+            if (currentMappingContext) {
+                validator = currentMappingContext.getEntityValidator(persistentEntity)
             }
         }
-        internalValidator
+        return validator
     }
 
     void setValidator(Validator validator) {
@@ -98,10 +151,15 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
             deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments)
         }
 
-        if (hasDatastore) {
-            currentSession = datastore.currentSession
-            previousFlushMode = currentSession.flushMode
-            currentSession.setFlushMode(FlushModeType.COMMIT)
+        if (hasDatastore && getDatastore().hasCurrentSession()) {
+            try {
+                currentSession = getDatastore().currentSession
+                previousFlushMode = currentSession.flushMode
+                currentSession.setFlushMode(FlushModeType.COMMIT)
+            } catch (IllegalStateException e) {
+                // Ignore, session might be disconnected
+                currentSession = null
+            }
         }
         try {
             beforeValidateHelper.invokeBeforeValidate(instance, fields)
@@ -196,11 +254,15 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
     private void fireEvent(target, List fields) {
         ValidationEvent event = createValidationEvent(target)
         event.validatedFields = fields
-        eventPublisher?.publishEvent(event)
+        ApplicationEventPublisher publisher = eventPublisher
+        if (publisher == null) {
+            publisher = getDatastore()?.getApplicationEventPublisher()
+        }
+        publisher?.publishEvent(event)
     }
 
     protected ValidationEvent createValidationEvent(target) {
-        new ValidationEvent(datastore, target)
+        new ValidationEvent(getDatastore(), target)
     }
 
     /**
@@ -228,12 +290,20 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
             return errors
         }
         else {
-
-            Errors errors = (Errors) datastore.currentSession.getAttribute(instance, GormProperties.ERRORS)
-            if (errors == null) {
-                errors = resetErrors(instance)
+            Datastore ds = getDatastore()
+            if (ds != null && ds.hasCurrentSession()) {
+                try {
+                    Errors errors = (Errors) ds.getCurrentSession().getAttribute(instance, GormProperties.ERRORS)
+                    if (errors == null) {
+                        errors = resetErrors(instance)
+                    }
+                    return errors
+                } catch (IllegalStateException e) {
+                    return new ValidationErrors(instance)
+                }
+            } else {
+                return new ValidationErrors(instance)
             }
-            return errors
         }
     }
 
@@ -254,7 +324,14 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
             gv.errors = errors
         }
         else {
-            datastore.currentSession.setAttribute(instance, GormProperties.ERRORS, errors)
+            Datastore ds = getDatastore()
+            if (ds != null && ds.hasCurrentSession()) {
+                try {
+                    ds.getCurrentSession().setAttribute(instance, GormProperties.ERRORS, errors)
+                } catch (IllegalStateException e) {
+                    // Ignore, session might be disconnected
+                }
+            }
         }
     }
 
@@ -277,8 +354,16 @@ class GormValidationApi<D> extends AbstractGormApi<D> {
             return gv.hasErrors()
         }
         else {
-            Errors errors = (Errors) datastore.currentSession.getAttribute(instance, GormProperties.ERRORS)
-            errors?.hasErrors()
+            Datastore ds = getDatastore()
+            if (ds != null && ds.hasCurrentSession()) {
+                try {
+                    Errors errors = (Errors) ds.getCurrentSession().getAttribute(instance, GormProperties.ERRORS)
+                    return errors?.hasErrors() ?: false
+                } catch (IllegalStateException e) {
+                    return false
+                }
+            }
+            return false
         }
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApiRegistry.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApiRegistry.groovy
new file mode 100644
index 0000000000..c0377baf48
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormValidationApiRegistry.groovy
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.model.MappingContext
+
+@CompileStatic
+class GormValidationApiRegistry extends AbstractGormApiRegistry<GormValidationApi> {
+
+    GormValidationApiRegistry(GormRegistry registry) {
+        super(registry)
+    }
+
+    @Override
+    protected GormValidationApi qualify(GormValidationApi api, String qualifier) {
+        Class persistentClass = api.persistentClass
+        Datastore datastore = registry.apiResolver.findDatastore(persistentClass, qualifier)
+        if (datastore == null) {
+            return api
+        }
+        MappingContext mappingContext = datastore.mappingContext
+        DatastoreResolver resolver = registry.createClassDatastoreResolver(persistentClass, qualifier)
+        return registry.getApiFactory(datastore).createValidationApi(persistentClass, mappingContext, resolver, registry)
+    }
+
+    <D> GormValidationApi<D> findValidationApi(Class<D> entity, String qualifier = null) {
+        String className = className(entity)
+        GormValidationApi api = get(className)
+        if (api == null) {
+            throw stateException(entity)
+        }
+
+        if (qualifier != null && qualifier != ConnectionSource.DEFAULT) {
+            return api.forQualifier(qualifier)
+        }
+        return (GormValidationApi<D>) api
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java
index f8757121a7..0887be76f8 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java
@@ -29,6 +29,9 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationContext;
@@ -62,6 +65,8 @@ import org.grails.datastore.mapping.model.PersistentProperty;
  */
 public class AutoTimestampEventListener extends AbstractPersistenceEventListener implements MappingContext.Listener, ApplicationContextAware {
 
+    private static final Logger LOG = LoggerFactory.getLogger(AutoTimestampEventListener.class);
+
     // if false, will not set timestamp on insert event if value is not null
     @Value("${" + Settings.SETTING_AUTO_TIMESTAMP_INSERT_OVERWRITE + ":true}")
     public boolean insertOverwrite = true;
@@ -218,7 +223,7 @@ public class AutoTimestampEventListener extends AbstractPersistenceEventListener
         return true;
     }
 
-    protected Set<String> getLastUpdatedPropertyNames(String entityName) {
+    public Set<String> getLastUpdatedPropertyNames(String entityName) {
         Optional<Set<String>> properties = entitiesWithLastUpdated.get(entityName);
         return properties == null ? null : properties.orElse(null);
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFindByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFindByFinder.java
index e2b03e51e0..8f9e42a4ff 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFindByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFindByFinder.java
@@ -16,11 +16,11 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.grails.datastore.gorm.finders;
 
 import java.util.regex.Pattern;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.Session;
 import org.grails.datastore.mapping.core.SessionCallback;
@@ -36,13 +36,17 @@ public abstract class AbstractFindByFinder extends DynamicFinder {
         super(pattern, OPERATORS, datastore);
     }
 
+    protected AbstractFindByFinder(Pattern pattern, String[] operators, DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(pattern, operators, datastoreResolver, mappingContext);
+    }
+
     protected AbstractFindByFinder(Pattern pattern, MappingContext mappingContext) {
         super(pattern, OPERATORS, mappingContext);
     }
 
     @Override
     protected Object doInvokeInternal(final DynamicFinderInvocation invocation) {
-        return execute(new SessionCallback<>() {
+        return execute(new SessionCallback<Object>() {
             public Object doInSession(final Session session) {
                 Query query = buildQuery(invocation, session);
                 adjustQuery(query);
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFinder.java
index fc23f59795..3c44da7286 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/AbstractFinder.java
@@ -21,6 +21,7 @@ package org.grails.datastore.gorm.finders;
 import groovy.lang.Closure;
 
 import grails.gorm.CriteriaBuilder;
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.DatastoreUtils;
 import org.grails.datastore.mapping.core.SessionCallback;
@@ -35,27 +36,41 @@ import org.grails.datastore.mapping.query.Query;
 @SuppressWarnings("rawtypes")
 public abstract class AbstractFinder implements FinderMethod {
 
-    protected final Datastore datastore;
+    protected DatastoreResolver datastoreResolver;
+    protected Datastore datastore;
 
     public AbstractFinder(final Datastore datastore) {
         this.datastore = datastore;
     }
 
+    public AbstractFinder(final DatastoreResolver datastoreResolver) {
+        this.datastoreResolver = datastoreResolver;
+    }
+
+    protected Datastore getDatastore() {
+        if (datastoreResolver != null) {
+            return datastoreResolver.resolve();
+        }
+        return datastore;
+    }
+
     protected <T> T execute(final SessionCallback<T> callback) {
-        if (datastore != null) {
-            return DatastoreUtils.execute(datastore, callback);
+        Datastore ds = getDatastore();
+        if (ds != null) {
+            return DatastoreUtils.execute(ds, callback);
         }
         else {
-            throw new IllegalStateException("Cannot execute session query in stateless mode");
+            throw new IllegalStateException("Cannot execute session query with null datastore");
         }
     }
 
     protected void execute(final VoidSessionCallback callback) {
-        if (datastore != null) {
-            DatastoreUtils.execute(datastore, callback);
+        Datastore ds = getDatastore();
+        if (ds != null) {
+            DatastoreUtils.execute(ds, callback);
         }
         else {
-            throw new IllegalStateException("Cannot execute session query in stateless mode");
+            throw new IllegalStateException("Cannot execute session query with null datastore");
         }
 
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/CountByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/CountByFinder.java
index 499c07cd45..3b835b1042 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/CountByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/CountByFinder.java
@@ -20,6 +20,8 @@ package org.grails.datastore.gorm.finders;
 
 import java.util.regex.Pattern;
 
+import grails.gorm.DetachedCriteria;
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.Session;
 import org.grails.datastore.mapping.core.SessionCallback;
@@ -27,55 +29,49 @@ import org.grails.datastore.mapping.model.MappingContext;
 import org.grails.datastore.mapping.query.Query;
 
 /**
- * Supports counting objects. For example Book.countByTitle("The Stand")
+ * Supports countBy* queries.
+ *
+ * @author Graeme Rocher
  */
 public class CountByFinder extends DynamicFinder implements QueryBuildingFinder {
 
-    private static final String OPERATOR_OR = "Or";
-    private static final String OPERATOR_AND = "And";
-
-    private static final Pattern METHOD_PATTERN = Pattern.compile("(countBy)(\\w+)");
-    private static final String[] OPERATORS = { OPERATOR_AND, OPERATOR_OR };
+    private static final String METHOD_PATTERN = "(countBy)([A-Z]\\w*)";
+    protected static final String[] OPERATORS = { "And", "Or" };
 
     public CountByFinder(final Datastore datastore) {
-        super(METHOD_PATTERN, OPERATORS, datastore);
+        super(Pattern.compile(METHOD_PATTERN), OPERATORS, datastore);
+    }
+
+    public CountByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(Pattern.compile(METHOD_PATTERN), OPERATORS, datastoreResolver, mappingContext);
     }
 
     public CountByFinder(MappingContext mappingContext) {
-        super(METHOD_PATTERN, OPERATORS, mappingContext);
+        super(Pattern.compile(METHOD_PATTERN), OPERATORS, mappingContext);
     }
 
     @Override
     protected Object doInvokeInternal(final DynamicFinderInvocation invocation) {
         return execute(new SessionCallback<Object>() {
             public Object doInSession(final Session session) {
-                Query query = buildQuery(invocation, session);
-                return invokeQuery(query);
+                Query q = buildQuery(invocation, session);
+                q.projections().count();
+                return q.singleResult();
             }
         });
     }
 
-    protected Object invokeQuery(Query q) {
-        return q.singleResult();
-    }
-
+    @Override
     public Query buildQuery(DynamicFinderInvocation invocation, Session session) {
-        final Class<?> clazz = invocation.getJavaClass();
+        final Class clazz = invocation.getJavaClass();
         Query q = session.createQuery(clazz);
-        return buildQuery(invocation, clazz, q);
-    }
-
-    protected Query buildQuery(DynamicFinderInvocation invocation, Class<?> clazz, Query q) {
-        applyAdditionalCriteria(q, invocation.getCriteria());
         applyDetachedCriteria(q, invocation.getDetachedCriteria());
-        configureQueryWithArguments(clazz, q, invocation.getArguments());
 
-        String operatorInUse = invocation.getOperator();
-        if (operatorInUse != null && operatorInUse.equals(OPERATOR_OR)) {
+        final String operator = invocation.getOperator();
+        if (operator != null && operator.equals("Or")) {
             Query.Junction disjunction = q.disjunction();
-
             for (MethodExpression expression : invocation.getExpressions()) {
-                q.add(disjunction, expression.createCriterion());
+                disjunction.add(expression.createCriterion());
             }
         }
         else {
@@ -83,8 +79,13 @@ public class CountByFinder extends DynamicFinder implements QueryBuildingFinder
                 q.add(expression.createCriterion());
             }
         }
-
-        q.projections().count();
         return q;
     }
+
+    protected void applyDetachedCriteria(Query q, DetachedCriteria detachedCriteria) {
+        if (detachedCriteria != null) {
+            DynamicFinder.applyDetachedCriteria(q, detachedCriteria);
+        }
+    }
+
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/DynamicFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/DynamicFinder.java
index 8792a485d8..dc8a7d7d85 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/DynamicFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/DynamicFinder.java
@@ -36,6 +36,7 @@ import org.springframework.core.convert.ConversionService;
 import org.springframework.util.StringUtils;
 
 import grails.gorm.DetachedCriteria;
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.gorm.finders.MethodExpression.Between;
 import org.grails.datastore.gorm.finders.MethodExpression.Equal;
 import org.grails.datastore.gorm.finders.MethodExpression.GreaterThan;
@@ -53,11 +54,13 @@ import org.grails.datastore.gorm.finders.MethodExpression.Like;
 import org.grails.datastore.gorm.finders.MethodExpression.NotEqual;
 import org.grails.datastore.gorm.finders.MethodExpression.NotInList;
 import org.grails.datastore.gorm.finders.MethodExpression.Rlike;
+import org.grails.datastore.gorm.query.criteria.AbstractCriteriaBuilder;
 import org.grails.datastore.gorm.query.criteria.AbstractDetachedCriteria;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.Session;
 import org.grails.datastore.mapping.model.MappingContext;
 import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
 import org.grails.datastore.mapping.model.types.Basic;
 import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.query.api.BuildableCriteria;
@@ -132,6 +135,15 @@ public abstract class DynamicFinder extends AbstractFinder implements QueryBuild
         resetMethodExpressionPattern();
     }
 
+    protected DynamicFinder(final Pattern pattern, final String[] operators, final DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(datastoreResolver);
+        this.mappingContext = mappingContext;
+        this.pattern = pattern;
+        this.operators = operators;
+        this.operatorPatterns = new Pattern[operators.length];
+        populateOperators(operators);
+    }
+
     protected DynamicFinder(final Pattern pattern, final String[] operators, final Datastore datastore) {
         super(datastore);
         this.mappingContext = datastore.getMappingContext();
@@ -142,7 +154,7 @@ public abstract class DynamicFinder extends AbstractFinder implements QueryBuild
     }
 
     protected DynamicFinder(final Pattern pattern, final String[] operators, final MappingContext mappingContext) {
-        super(null);
+        super((Datastore) null);
         this.mappingContext = mappingContext;
         this.pattern = pattern;
         this.operators = operators;
@@ -433,6 +445,31 @@ public abstract class DynamicFinder extends AbstractFinder implements QueryBuild
         }
 
         Object sortObject = argMap.get(ARGUMENT_SORT);
+        if (sortObject == null && orderParam != null) {
+            PersistentEntity entity = null;
+            if (query instanceof AbstractCriteriaBuilder) {
+                entity = ((AbstractCriteriaBuilder) query).getPersistentEntity();
+            }
+            else if (query instanceof AbstractDetachedCriteria) {
+                entity = ((AbstractDetachedCriteria) query).getPersistentEntity();
+            }
+
+            if (entity != null) {
+                PersistentProperty identity = entity.getIdentity();
+                if (identity != null) {
+                    sortObject = identity.getName();
+                } else {
+                    PersistentProperty[] composite = entity.getCompositeIdentity();
+                    if (composite != null && composite.length > 0) {
+                        Map<String, String> sortMap = new LinkedHashMap<>();
+                        for (PersistentProperty p : composite) {
+                            sortMap.put(p.getName(), orderParam);
+                        }
+                        sortObject = sortMap;
+                    }
+                }
+            }
+        }
         boolean ignoreCase = !argMap.containsKey(ARGUMENT_IGNORE_CASE) || ClassUtils.getBooleanFromMap(ARGUMENT_IGNORE_CASE, argMap);
 
         if (sortObject != null) {
@@ -521,6 +558,24 @@ public abstract class DynamicFinder extends AbstractFinder implements QueryBuild
             query.offset(offset);
         }
         Object sortObject = argMap.get(ARGUMENT_SORT);
+        if (sortObject == null && orderParam != null) {
+            PersistentEntity entity = query.getEntity();
+            if (entity != null) {
+                PersistentProperty identity = entity.getIdentity();
+                if (identity != null) {
+                    sortObject = identity.getName();
+                } else {
+                    PersistentProperty[] composite = entity.getCompositeIdentity();
+                    if (composite != null && composite.length > 0) {
+                        Map<String, String> sortMap = new LinkedHashMap<>();
+                        for (PersistentProperty p : composite) {
+                            sortMap.put(p.getName(), orderParam);
+                        }
+                        sortObject = sortMap;
+                    }
+                }
+            }
+        }
         boolean ignoreCase = !argMap.containsKey(ARGUMENT_IGNORE_CASE) || ClassUtils.getBooleanFromMap(ARGUMENT_IGNORE_CASE, argMap);
 
         if (sortObject != null) {
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByBooleanFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByBooleanFinder.java
index ce3e03aa1d..3bf787abd0 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByBooleanFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByBooleanFinder.java
@@ -16,24 +16,19 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.grails.datastore.gorm.finders;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.model.MappingContext;
 
 /**
- * The "findAll&lt;booleanProperty&gt;By*" static persistent method. This method allows querying for
- * instances of grails domain classes based on a boolean property and any other arbitrary
- * properties.
+ * The "findAll<booleanProperty>By*" static persistent method. This method allows querying for
+ * instances of initial boolean property and additional criteria on other properties.
  *
  * eg.
- * Account.findAllActiveByHolder("Joe Blogs"); // Where class "Account" has a properties called "active" and "holder"
- * Account.findAllActiveByHolderAndBranch("Joe Blogs", "London"); // Where class "Account" has a properties called "active', "holder" and "branch"
- *
- * In both of those queries, the query will only select Account objects where active=true.
+ * Book.findAllActiveByTitle("The Stand")
  *
- * @author Jeff Brown
  * @author Graeme Rocher
  */
 public class FindAllByBooleanFinder extends FindAllByFinder {
@@ -44,6 +39,11 @@ public class FindAllByBooleanFinder extends FindAllByFinder {
         setPattern(METHOD_PATTERN);
     }
 
+    public FindAllByBooleanFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(datastoreResolver, mappingContext);
+        setPattern(METHOD_PATTERN);
+    }
+
     public FindAllByBooleanFinder(MappingContext mappingContext) {
         super(mappingContext);
         setPattern(METHOD_PATTERN);
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByFinder.java
index ed1eaede5e..8027851bc3 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindAllByFinder.java
@@ -20,6 +20,7 @@ package org.grails.datastore.gorm.finders;
 
 import java.util.regex.Pattern;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.Session;
 import org.grails.datastore.mapping.core.SessionCallback;
@@ -40,7 +41,11 @@ public class FindAllByFinder extends DynamicFinder {
         super(Pattern.compile(METHOD_PATTERN), OPERATORS, datastore);
     }
 
-    public FindAllByFinder(final MappingContext mappingContext) {
+    public FindAllByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(Pattern.compile(METHOD_PATTERN), OPERATORS, datastoreResolver, mappingContext);
+    }
+
+    public FindAllByFinder(MappingContext mappingContext) {
         super(Pattern.compile(METHOD_PATTERN), OPERATORS, mappingContext);
     }
 
@@ -55,12 +60,12 @@ public class FindAllByFinder extends DynamicFinder {
         });
     }
 
-    protected Object invokeQuery(Query q) {
-        return q.list();
+    protected Object invokeQuery(Query query) {
+        return query.list();
     }
 
     protected void adjustQuery(Query query) {
-        query.projections().distinct();
+        // do nothing
     }
 
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByBooleanFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByBooleanFinder.java
index c1591f9398..c4e24a7178 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByBooleanFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByBooleanFinder.java
@@ -18,36 +18,27 @@
  */
 package org.grails.datastore.gorm.finders;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.model.MappingContext;
 
 /**
- * 
- * <p>The "find&lt;booleanProperty&gt;By*" static persistent method. This method allows querying for
- * instances of grails domain classes based on a boolean property and any other arbitrary
- * properties. This method returns the first result of the query.</p>
- *
- * <pre><code>
- * eg.
- * Account.findActiveByHolder("Joe Blogs"); // Where class "Account" has a properties called "active" and "holder"
- * Account.findActiveByHolderAndBranch("Joe Blogs", "London"); // Where class "Account" has a properties called "active', "holder" and "branch"
- * </code></pre>
- *
- * <p>
- * In both of those queries, the query will only select Account objects where active=true.
- * </p>
- *
  * @author Graeme Rocher
- * @author Jeff Brown
+ * @since 1.0
  */
 public class FindByBooleanFinder extends FindByFinder {
-    public static final String METHOD_PATTERN = "(find)((\\w+)(By)([A-Z]\\w*)|(\\w++))";
+    public static final String METHOD_PATTERN = "(find)((\\w+)(By)([A-Z]\\w*)|(\\w+))";
 
     public FindByBooleanFinder(Datastore datastore) {
         super(datastore);
         setPattern(METHOD_PATTERN);
     }
 
+    public FindByBooleanFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(datastoreResolver, mappingContext);
+        setPattern(METHOD_PATTERN);
+    }
+
     public FindByBooleanFinder(MappingContext mappingContext) {
         super(mappingContext);
         setPattern(METHOD_PATTERN);
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java
index 5e8ee5cf61..28f7815b39 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindByFinder.java
@@ -20,6 +20,7 @@ package org.grails.datastore.gorm.finders;
 
 import java.util.regex.Pattern;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.model.MappingContext;
 
@@ -34,6 +35,10 @@ public class FindByFinder extends AbstractFindByFinder {
         super(Pattern.compile(METHOD_PATTERN), datastore);
     }
 
+    public FindByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(Pattern.compile(METHOD_PATTERN), OPERATORS, datastoreResolver, mappingContext);
+    }
+
     public FindByFinder(MappingContext mappingContext) {
         super(Pattern.compile(METHOD_PATTERN), mappingContext);
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrCreateByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrCreateByFinder.java
index 8205dec00b..3fda6c9e9d 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrCreateByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrCreateByFinder.java
@@ -29,6 +29,7 @@ import groovy.lang.MissingMethodException;
 
 import org.springframework.core.convert.ConversionException;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.exceptions.ConfigurationException;
 import org.grails.datastore.mapping.model.MappingContext;
@@ -44,10 +45,18 @@ public class FindOrCreateByFinder extends AbstractFindByFinder {
         super(Pattern.compile(methodPattern), datastore);
     }
 
+    public FindOrCreateByFinder(final String methodPattern, DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(Pattern.compile(methodPattern), OPERATORS, datastoreResolver, mappingContext);
+    }
+
     public FindOrCreateByFinder(final Datastore datastore) {
         this(METHOD_PATTERN, datastore);
     }
 
+    public FindOrCreateByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        this(METHOD_PATTERN, datastoreResolver, mappingContext);
+    }
+
     public FindOrCreateByFinder(MappingContext mappingContext) {
         super(Pattern.compile(METHOD_PATTERN), mappingContext);
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrSaveByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrSaveByFinder.java
index 3552214b8f..d31ce7140f 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrSaveByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/FindOrSaveByFinder.java
@@ -18,14 +18,7 @@
  */
 package org.grails.datastore.gorm.finders;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import groovy.lang.GroovySystem;
-import groovy.lang.MetaClass;
-import groovy.lang.MissingMethodException;
-
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.model.MappingContext;
 
@@ -33,37 +26,24 @@ public class FindOrSaveByFinder extends FindOrCreateByFinder {
 
     public static final String METHOD_PATTERN = "(findOrSaveBy)([A-Z]\\w*)";
 
-    public FindOrSaveByFinder(final Datastore datastore) {
-        super(METHOD_PATTERN, datastore);
+    public FindOrSaveByFinder(final String methodPattern, final Datastore datastore) {
+        super(methodPattern, datastore);
     }
 
-    public FindOrSaveByFinder(final MappingContext mappingContext) {
-        super(METHOD_PATTERN, mappingContext);
+    public FindOrSaveByFinder(final String methodPattern, DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(methodPattern, datastoreResolver, mappingContext);
     }
 
-    @Override
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    protected Object doInvokeInternal(final DynamicFinderInvocation invocation) {
-        if (OPERATOR_OR.equals(invocation.getOperator())) {
-            throw new MissingMethodException(invocation.getMethodName(), invocation.getJavaClass(), invocation.getArguments());
-        }
+    public FindOrSaveByFinder(Datastore datastore) {
+        super(METHOD_PATTERN, datastore);
+    }
 
-        Object result = super.doInvokeInternal(invocation);
-        if (result == null) {
-            Map m = new HashMap();
-            List<MethodExpression> expressions = invocation.getExpressions();
-            for (MethodExpression me : expressions) {
-                if (!(me instanceof MethodExpression.Equal)) {
-                    throw new MissingMethodException(invocation.getMethodName(), invocation.getJavaClass(), invocation.getArguments());
-                }
-                String propertyName = me.propertyName;
-                Object[] arguments = me.getArguments();
-                m.put(propertyName, arguments[0]);
-            }
-            MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(invocation.getJavaClass());
-            result = metaClass.invokeConstructor(new Object[]{m});
-        }
-        return result;
+    public FindOrSaveByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(METHOD_PATTERN, datastoreResolver, mappingContext);
+    }
+
+    public FindOrSaveByFinder(MappingContext mappingContext) {
+        super(METHOD_PATTERN, mappingContext);
     }
 
     @Override
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/ListOrderByFinder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/ListOrderByFinder.java
index 1f522b9554..c8f7e4849c 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/ListOrderByFinder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/finders/ListOrderByFinder.java
@@ -25,18 +25,21 @@ import java.util.regex.Pattern;
 
 import groovy.lang.Closure;
 
+import org.grails.datastore.gorm.DatastoreResolver;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.Session;
 import org.grails.datastore.mapping.core.SessionCallback;
+import org.grails.datastore.mapping.model.MappingContext;
 import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.reflect.NameUtils;
 
 /**
- * The "listOrderBy*" static persistent method. Allows ordered listing of instances based on their properties.
+ * The "listOrderBy*" static persistent method. This method allows queries on the properties of the class of the form
+ * listOrderBy[Property]([Map] args)
  *
  * eg.
- * Account.listOrderByHolder();
- * Account.listOrderByHolder(max); // max results
+ * Book.listOrderByTitle(max:10)
+ * Book.listOrderByTitleAndAuthor(max:10)
  *
  * @author Graeme Rocher
  */
@@ -44,56 +47,62 @@ public class ListOrderByFinder extends AbstractFinder {
     private static final Pattern METHOD_PATTERN = Pattern.compile("(listOrderBy)(\\w+)");
     private Pattern pattern = METHOD_PATTERN;
 
-    public ListOrderByFinder(Datastore datastore) {
+    public ListOrderByFinder(final Datastore datastore) {
         super(datastore);
     }
 
+    public ListOrderByFinder(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
+        super(datastoreResolver);
+    }
+
     public void setPattern(String pattern) {
         this.pattern = Pattern.compile(pattern);
     }
 
-    @SuppressWarnings("rawtypes")
+    public boolean isMethodMatch(String methodName) {
+        return pattern.matcher(methodName).find();
+    }
+
+    @Override
     public Object invoke(final Class clazz, final String methodName, final Object[] arguments) {
         return invoke(clazz, methodName, null, arguments);
     }
 
-    @SuppressWarnings("rawtypes")
+    @Override
     public Object invoke(final Class clazz, final String methodName, final Closure additionalCriteria, final Object[] arguments) {
-
-        Matcher match = pattern.matcher(methodName);
-        match.find();
-
-        String nameInSignature = match.group(2);
-        final String propertyName = NameUtils.decapitalizeFirstChar(nameInSignature);
-
-        return execute(new SessionCallback<>() {
+        return execute(new SessionCallback<Object>() {
+            @Override
             public Object doInSession(final Session session) {
-                Query q = session.createQuery(clazz);
-                applyAdditionalCriteria(q, additionalCriteria);
+                final Matcher matcher = pattern.matcher(methodName);
+                matcher.find();
+                String parts = matcher.group(2);
+
+                final Query q = session.createQuery(clazz);
+                String[] propertyNames = parts.split("And");
+                for (String propertyName : propertyNames) {
+                    q.order(Query.Order.asc(NameUtils.decapitalize(propertyName)));
+                }
 
-                boolean ascending = true;
                 if (arguments.length > 0 && (arguments[0] instanceof Map)) {
-                    final Map args = new LinkedHashMap((Map) arguments[0]);
-                    final Object order = args.remove(DynamicFinder.ARGUMENT_ORDER);
-                    if (order != null && "desc".equalsIgnoreCase(order.toString())) {
-                        ascending = false;
+                    Map args = new LinkedHashMap((Map) arguments[0]);
+                    final Object order = args.remove("order");
+                    if (order != null) {
+                        if (order.toString().equalsIgnoreCase("desc")) {
+                            q.clearOrders();
+                            for (String propertyName : propertyNames) {
+                                q.order(Query.Order.desc(NameUtils.decapitalize(propertyName)));
+                            }
+                        }
                     }
                     DynamicFinder.populateArgumentsForCriteria(clazz, q, args);
                 }
-
-                q.order(ascending ? Query.Order.asc(propertyName) : Query.Order.desc(propertyName));
-                q.projections().distinct();
-                return invokeQuery(q);
+                
+                if (additionalCriteria != null) {
+                    applyAdditionalCriteria(q, additionalCriteria);
+                }
+                
+                return q.list();
             }
         });
     }
-
-    protected Object invokeQuery(Query q) {
-        return q.list();
-    }
-
-    public boolean isMethodMatch(String methodName) {
-        return pattern.matcher(methodName.subSequence(0, methodName.length())).find();
-    }
-
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnection.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnection.groovy
index ccc7983879..74ad158ecf 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnection.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnection.groovy
@@ -45,12 +45,6 @@ class MultiTenantConnection implements Connection {
 
     @Override
     void close() throws SQLException {
-        try {
-            if (!isClosed()) {
-                schemaHandler.useDefaultSchema(this)
-            }
-        } finally {
-            target.close()
-        }
+        target.close()
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy
index 964e957180..d994f28881 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/schema/DefaultSchemaHandler.groovy
@@ -56,6 +56,7 @@ class DefaultSchemaHandler implements SchemaHandler {
     @Override
     void useSchema(Connection connection, String name) {
         String useStatement = String.format(useSchemaStatement, quoteName(connection, name))
+        System.err.println "Executing SQL: ${useStatement}"
         log.debug('Executing SQL Set Schema Statement: {}', useStatement)
         connection
                 .createStatement()
@@ -64,12 +65,14 @@ class DefaultSchemaHandler implements SchemaHandler {
 
     @Override
     void useDefaultSchema(Connection connection) {
+        System.err.println "Executing SQL: useDefaultSchema (${defaultSchemaName})"
         useSchema(connection, defaultSchemaName)
     }
 
     @Override
     void createSchema(Connection connection, String name) {
         String schemaCreateStatement = String.format(createSchemaStatement, quoteName(connection, name))
+        System.err.println "Executing SQL: ${schemaCreateStatement}"
         log.debug('Executing SQL Create Schema Statement: {}', schemaCreateStatement)
         connection
                 .createStatement()
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/MultiTenantEventListener.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/MultiTenantEventListener.java
index b95d62fbaa..7fb3841cf7 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/MultiTenantEventListener.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/MultiTenantEventListener.java
@@ -16,17 +16,19 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.grails.datastore.gorm.multitenancy;
 
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.List;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.springframework.context.ApplicationEvent;
 
 import grails.gorm.multitenancy.Tenants;
-import org.grails.datastore.gorm.GormEnhancer;
+import org.grails.datastore.gorm.GormRegistry;
 import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.core.connections.ConnectionSource;
 import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent;
@@ -48,6 +50,7 @@ import org.grails.datastore.mapping.query.event.PreQueryEvent;
  * @since 6.0
  */
 public class MultiTenantEventListener implements PersistenceEventListener {
+    private static final Logger LOG = LoggerFactory.getLogger(MultiTenantEventListener.class);
     protected final Datastore datastore;
     public static final List<Class<? extends ApplicationEvent>> SUPPORTED_EVENTS = Arrays.asList(PreQueryEvent.class, ValidationEvent.class, PreInsertEvent.class, PreUpdateEvent.class);
 
@@ -62,13 +65,22 @@ public class MultiTenantEventListener implements PersistenceEventListener {
 
     @Override
     public boolean supportsSourceType(Class<?> sourceType) {
-        return Datastore.class.isAssignableFrom(sourceType);
+        return datastore.getClass().isAssignableFrom(sourceType);
+    }
+
+    private boolean isValidSource(ApplicationEvent event) {
+        Object source = event.getSource();
+        if (source instanceof Datastore) {
+            Datastore eventDatastore = (Datastore) source;
+            return this.datastore.equals(eventDatastore);
+        }
+        return false;
     }
 
     @Override
     public void onApplicationEvent(ApplicationEvent event) {
-        Class<? extends ApplicationEvent> eventClass = event.getClass();
-        if (supportsEventType(eventClass)) {
+        if (isValidSource(event)) {
+            Class<? extends ApplicationEvent> eventClass = event.getClass();
             Datastore datastore = (Datastore) event.getSource();
             if (event instanceof PreQueryEvent) {
                 PreQueryEvent preQueryEvent = (PreQueryEvent) event;
@@ -77,20 +89,24 @@ public class MultiTenantEventListener implements PersistenceEventListener {
                 PersistentEntity entity = query.getEntity();
                 if (entity.isMultiTenant()) {
                     if (datastore == null) {
-                        datastore = GormEnhancer.findDatastore(entity.getJavaClass());
+                        datastore = GormRegistry.getInstance().getApiResolver().findDatastore(entity.getJavaClass());
                     }
                     if (supportsSourceType(datastore.getClass()) && this.datastore.equals(datastore)) {
                         TenantId tenantId = entity.getTenantId();
                         if (tenantId != null) {
                             Serializable currentId;
-
                             if (datastore instanceof MultiTenantCapableDatastore) {
                                 currentId = Tenants.currentId((MultiTenantCapableDatastore) datastore);
-                            }
-                            else {
+                            } else {
                                 currentId = Tenants.currentId(datastore.getClass());
                             }
-                            query.eq(tenantId.getName(), currentId);
+
+                            if (currentId != null) {
+                                if (ConnectionSource.DEFAULT.equals(currentId) && Number.class.isAssignableFrom(tenantId.getType())) {
+                                    currentId = 0L;
+                                }
+                                query.eq(tenantId.getName(), currentId);
+                            }
                         }
                     }
                 }
@@ -101,26 +117,29 @@ public class MultiTenantEventListener implements PersistenceEventListener {
                 if (entity.isMultiTenant()) {
                     TenantId tenantId = entity.getTenantId();
                     if (datastore == null) {
-                        datastore = GormEnhancer.findDatastore(entity.getJavaClass());
+                        datastore = GormRegistry.getInstance().getApiResolver().findDatastore(entity.getJavaClass());
                     }
                     if (supportsSourceType(datastore.getClass()) && this.datastore.equals(datastore)) {
-                        Serializable currentId;
+                        Serializable currentId = null;
+                        try {
+                            if (datastore instanceof MultiTenantCapableDatastore) {
+                                currentId = Tenants.currentId((MultiTenantCapableDatastore) datastore);
+                            } else {
+                                currentId = Tenants.currentId(datastore.getClass());
+                            }
 
-                        if (datastore instanceof MultiTenantCapableDatastore) {
-                            currentId = Tenants.currentId((MultiTenantCapableDatastore) datastore);
-                        }
-                        else {
-                            currentId = Tenants.currentId(datastore.getClass());
-                        }
-                        if (currentId != null) {
-                            try {
-                                if (currentId == ConnectionSource.DEFAULT) {
-                                    currentId = (Serializable) preInsertEvent.getEntityAccess().getProperty(tenantId.getName());
+                            if (currentId != null) {
+                                Object existingId = preInsertEvent.getEntityAccess().getProperty(tenantId.getName());
+                                if (existingId != null) {
+                                    currentId = (Serializable) existingId;
+                                }
+                                if (ConnectionSource.DEFAULT.equals(currentId) && Number.class.isAssignableFrom(tenantId.getType())) {
+                                    currentId = 0L;
                                 }
                                 preInsertEvent.getEntityAccess().setProperty(tenantId.getName(), currentId);
-                            } catch (Exception e) {
-                                throw new TenantException("Could not assigned tenant id [" + currentId + "] to property [" + tenantId + "], probably due to a type mismatch. You should return a type from the tenant resolver that matches the property type of the tenant id!: " + e.getMessage(), e);
                             }
+                        } catch (Exception e) {
+                            throw new TenantException("Could not assigned tenant id [" + currentId + "] to property [" + tenantId + "], probably due to a type mismatch. You should return a type from the tenant resolver that matches the property type of the tenant id!: " + e.getMessage(), e);
                         }
                     }
                 }
@@ -133,4 +152,3 @@ public class MultiTenantEventListener implements PersistenceEventListener {
         return DEFAULT_ORDER;
     }
 }
-
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/TenantDelegatingGormOperations.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/TenantDelegatingGormOperations.groovy
index fb01893dc4..930a51b8b1 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/TenantDelegatingGormOperations.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/TenantDelegatingGormOperations.groovy
@@ -236,6 +236,20 @@ class TenantDelegatingGormOperations<D> implements GormAllOperations<D> {
         }
     }
 
+    @Override
+    Number deleteAll() {
+        (Number)Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
+            allOperations.deleteAll()
+        }
+    }
+
+    @Override
+    Number deleteAll(Map params) {
+        (Number)Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
+            allOperations.deleteAll(params)
+        }
+    }
+
     @Override
     void deleteAll(Object... objectsToDelete) {
         Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
@@ -243,6 +257,13 @@ class TenantDelegatingGormOperations<D> implements GormAllOperations<D> {
         }
     }
 
+    @Override
+    void deleteAll(Map params, Object... objectsToDelete) {
+        Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
+            allOperations.deleteAll(params, objectsToDelete)
+        }
+    }
+
     @Override
     void deleteAll(Iterable objectsToDelete) {
         Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
@@ -250,6 +271,13 @@ class TenantDelegatingGormOperations<D> implements GormAllOperations<D> {
         }
     }
 
+    @Override
+    void deleteAll(Map params, Iterable objectsToDelete) {
+        Tenants.withId((Class<Datastore>) datastore.getClass(), tenantId) {
+            allOperations.deleteAll(params, objectsToDelete)
+        }
+    }
+
     @Override
     D create() {
         allOperations.create()
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
index 0e6b15ff60..161ec580ad 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
@@ -40,6 +40,7 @@ import grails.gorm.multitenancy.Tenant
 import grails.gorm.multitenancy.TenantService
 import grails.gorm.multitenancy.WithoutTenant
 import org.apache.grails.common.compiler.GroovyTransformOrder
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.transform.AbstractDatastoreMethodDecoratingTransformation
 import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
 import org.grails.datastore.mapping.reflect.AstUtils
@@ -62,6 +63,9 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt
 import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
 import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callD
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.grails.datastore.mapping.reflect.AstUtils.implementsInterface
+import static org.grails.datastore.mapping.reflect.AstUtils.findAnnotation
 import static org.grails.datastore.mapping.reflect.AstUtils.copyParameters
 import static org.grails.datastore.mapping.reflect.AstUtils.varThis
 
@@ -100,8 +104,29 @@ class TenantTransform extends AbstractDatastoreMethodDecoratingTransformation {
         VariableScope variableScope = methodNode.getVariableScope()
         VariableExpression tenantServiceVar = varX('$tenantService', tenantServiceClassNode)
         variableScope.putDeclaredVariable(tenantServiceVar)
+
+        Expression datastoreExpr
+        boolean isService = implementsInterface(classNode, 'org.grails.datastore.mapping.services.Service') ||
+                AstUtils.findAnnotation(classNode, grails.gorm.services.Service) != null
+
+        if (isService) {
+            // For services, resolve entirely via static bridge to avoid MetaClass recursion
+            def registryExpr = new org.codehaus.groovy.ast.expr.MethodCallExpression(classX(GormRegistry), 'getInstance', org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS)
+            def apiResolverExpr = new org.codehaus.groovy.ast.expr.MethodCallExpression(registryExpr, 'getApiResolver', org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS)
+            // Use the domain class from the @Service annotation
+            AnnotationNode serviceAnn = findAnnotation(classNode, grails.gorm.services.Service)
+            Expression domainClassExpr = serviceAnn?.getMember('value') ?: classX(org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE)
+            datastoreExpr = callX(apiResolverExpr, 'findDatastore', args(domainClassExpr))
+        }
+        else {
+            // Static bridge for regular objects too, to keep it stateless and avoid field injection
+            def registryExpr = new org.codehaus.groovy.ast.expr.MethodCallExpression(classX(GormRegistry), 'getInstance', org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS)
+            def apiResolverExpr = new org.codehaus.groovy.ast.expr.MethodCallExpression(registryExpr, 'getApiResolver', org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS)
+            datastoreExpr = callX(apiResolverExpr, 'findSingleDatastore')
+        }
+
         newMethodBody.addStatement(
-            declS(tenantServiceVar, callD(ServiceRegistry, 'targetDatastore', 'getService', classX(tenantServiceClassNode)))
+            declS(tenantServiceVar, callX(castX(make(ServiceRegistry), datastoreExpr), 'getService', classX(tenantServiceClassNode)))
         )
 
         ClassNode serializableClassNode = make(Serializable)
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
index 50e7943914..05e9462ff8 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java
@@ -93,12 +93,17 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         return this.targetClass;
     }
 
+    public PersistentEntity getPersistentEntity() {
+        return this.persistentEntity;
+    }
+
     public void setUniqueResult(boolean uniqueResult) {
         this.uniqueResult = uniqueResult;
     }
 
     @Override
     public Criteria cache(boolean cache) {
+        ensureQueryIsInitialized();
         query.cache(cache);
         return this;
     }
@@ -110,11 +115,13 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     public Criteria join(String property) {
+        ensureQueryIsInitialized();
         query.join(property);
         return this;
     }
 
     public Criteria select(String property) {
+        ensureQueryIsInitialized();
         query.select(property);
         return this;
     }
@@ -328,8 +335,16 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
         throw new MissingMethodException(name, getClass(), args);
     }
 
+    public List list(Closure callable) {
+        ensureQueryIsInitialized();
+        invokeClosureNode(callable);
+
+        return query.list();
+    }
+
     protected Object invokeList() {
         Object result;
+        ensureQueryIsInitialized();
         result = query.list();
         return result;
     }
@@ -341,6 +356,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @return The projections list
      */
     public ProjectionList projections(Closure callable) {
+        ensureQueryIsInitialized();
         projectionList = query.projections();
         invokeClosureNode(callable);
         return projectionList;
@@ -809,7 +825,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     /**
-     * Creates an "in" Criterion based on the specified property name and list of values.
+     * Creates an "in\" Criterion based on the specified property name and list of values.
      *
      * @param propertyName The property name
      * @param values The values
@@ -824,7 +840,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     /**
-     * Creates an "in" Criterion based on the specified property name and list of values.
+     * Creates an "in\" Criterion based on the specified property name and list of values.
      *
      * @param propertyName The property name
      * @param values The values
@@ -837,7 +853,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     /**
-     * Creates an "in" Criterion based on the specified property name and list of values.
+     * Creates an "in\" Criterion based on the specified property name and list of values.
      *
      * @param propertyName The property name
      * @param values The values
@@ -849,7 +865,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
     }
 
     /**
-     * Creates an "in" Criterion based on the specified property name and list of values.
+     * Creates an "in\" Criterion based on the specified property name and list of values.
      *
      * @param propertyName The property name
      * @param values The values
@@ -990,6 +1006,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * @return A Order instance
      */
     public Criteria order(String propertyName) {
+        ensureQueryIsInitialized();
         Query.Order o = Query.Order.asc(propertyName);
         if (paginationEnabledList) {
             orderEntries.add(o);
@@ -1008,6 +1025,7 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      */
     @Override
     public Criteria order(Query.Order o) {
+        ensureQueryIsInitialized();
         if (paginationEnabledList) {
             orderEntries.add(o);
         }
@@ -1021,11 +1039,12 @@ public abstract class AbstractCriteriaBuilder extends GroovyObjectSupport implem
      * Orders by the specified property name and direction
      *
      * @param propertyName The property name to order by
-     * @param direction Either "asc" for ascending or "desc" for descending
+     * @param direction Either "asc\" for ascending or \"desc\" for descending
      *
      * @return A Order instance
      */
     public Criteria order(String propertyName, String direction) {
+        ensureQueryIsInitialized();
         Query.Order o;
         if (direction.equals(CriteriaBuilder.ORDER_DESCENDING)) {
             o = Query.Order.desc(propertyName);
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTenantService.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTenantService.groovy
index c4700fe11d..2f398c1b34 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTenantService.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTenantService.groovy
@@ -22,6 +22,7 @@ import groovy.transform.CompileStatic
 
 import grails.gorm.multitenancy.TenantService
 import grails.gorm.multitenancy.Tenants
+import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.model.DatastoreConfigurationException
 import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
 import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
@@ -36,6 +37,20 @@ import org.grails.datastore.mapping.services.Service
 @CompileStatic
 class DefaultTenantService implements Service, TenantService {
 
+    private static final ThreadLocal<Boolean> RESOLVING = ThreadLocal.withInitial { false }
+
+    private Datastore datastore
+
+    @Override
+    Datastore getDatastore() {
+        return this.datastore
+    }
+
+    @Override
+    void setDatastore(Datastore datastore) {
+        this.datastore = datastore
+    }
+
     @Override
     void eachTenant(Closure callable) {
         MultiTenantCapableDatastore multiTenantCapableDatastore = multiTenantDatastore()
@@ -50,7 +65,7 @@ class DefaultTenantService implements Service, TenantService {
             return Tenants.currentId(multiTenantCapableDatastore)
         }
         else {
-            throw new DatastoreConfigurationException("Current datastore [$datastore] is not configured for Multi-Tenancy")
+            throw new DatastoreConfigurationException("Current datastore [${getDatastore()}] is not configured for Multi-Tenancy")
         }
     }
 
@@ -62,40 +77,57 @@ class DefaultTenantService implements Service, TenantService {
             return Tenants.withoutId(multiTenantCapableDatastore, callable)
         }
         else {
-            throw new DatastoreConfigurationException("Current datastore [$datastore] is not configured for Multi-Tenancy")
+            throw new DatastoreConfigurationException("Current datastore [${getDatastore()}] is not configured for Multi-Tenancy")
         }
     }
 
     @Override
     def <T> T withCurrent(Closure<T> callable) {
-        MultiTenantCapableDatastore multiTenantCapableDatastore = multiTenantDatastore()
-        def mode = multiTenantCapableDatastore.getMultiTenancyMode()
-        if (mode != MultiTenancySettings.MultiTenancyMode.NONE) {
-            return Tenants.withId(multiTenantCapableDatastore, currentId(), callable)
+        if (RESOLVING.get()) {
+            return (T)callable.call()
         }
-        else {
-            throw new DatastoreConfigurationException("Current datastore [$datastore] is not configured for Multi-Tenancy")
+        RESOLVING.set(true)
+        try {
+            MultiTenantCapableDatastore multiTenantCapableDatastore = multiTenantDatastore()
+            def mode = multiTenantCapableDatastore.getMultiTenancyMode()
+            if (mode != MultiTenancySettings.MultiTenancyMode.NONE) {
+                return Tenants.withId(multiTenantCapableDatastore, currentId(), callable)
+            }
+            else {
+                throw new DatastoreConfigurationException("Current datastore [${getDatastore()}] is not configured for Multi-Tenancy")
+            }
+        } finally {
+            RESOLVING.set(false)
         }
     }
 
     @Override
     def <T> T withId(Serializable tenantId, Closure<T> callable) {
-        MultiTenantCapableDatastore multiTenantCapableDatastore = multiTenantDatastore()
-        def mode = multiTenantCapableDatastore.getMultiTenancyMode()
-        if (mode != MultiTenancySettings.MultiTenancyMode.NONE) {
-            return Tenants.withId(multiTenantCapableDatastore, tenantId, callable)
+        if (RESOLVING.get()) {
+            return (T)callable.call()
         }
-        else {
-            throw new DatastoreConfigurationException("Current datastore [$datastore] is not configured for Multi-Tenancy")
+        RESOLVING.set(true)
+        try {
+            MultiTenantCapableDatastore multiTenantCapableDatastore = multiTenantDatastore()
+            def mode = multiTenantCapableDatastore.getMultiTenancyMode()
+            if (mode != MultiTenancySettings.MultiTenancyMode.NONE) {
+                return Tenants.withId(multiTenantCapableDatastore, tenantId, callable)
+            }
+            else {
+                throw new DatastoreConfigurationException("Current datastore [${getDatastore()}] is not configured for Multi-Tenancy")
+            }
+        } finally {
+            RESOLVING.set(false)
         }
     }
 
     protected MultiTenantCapableDatastore multiTenantDatastore() {
         MultiTenantCapableDatastore multiTenantCapableDatastore
-        if (datastore instanceof MultiTenantCapableDatastore) {
-            multiTenantCapableDatastore = (MultiTenantCapableDatastore) datastore
+        Datastore ds = getDatastore()
+        if (ds instanceof MultiTenantCapableDatastore) {
+            multiTenantCapableDatastore = (MultiTenantCapableDatastore) ds
         } else {
-            throw new DatastoreConfigurationException("Current datastore [$datastore] is not Multi-Tenant capable")
+            throw new DatastoreConfigurationException("Current datastore [$ds] is not Multi-Tenant capable")
         }
         return multiTenantCapableDatastore
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTransactionService.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTransactionService.groovy
index 27d9a4dce5..8298ce699c 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTransactionService.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/DefaultTransactionService.groovy
@@ -29,6 +29,7 @@ import org.springframework.transaction.TransactionSystemException
 
 import grails.gorm.transactions.GrailsTransactionTemplate
 import grails.gorm.transactions.TransactionService
+import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.services.Service
 import org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute
 import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
@@ -42,6 +43,18 @@ import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 @CompileStatic
 class DefaultTransactionService implements TransactionService, Service {
 
+    private Datastore datastore
+
+    @Override
+    Datastore getDatastore() {
+        return this.datastore
+    }
+
+    @Override
+    void setDatastore(Datastore datastore) {
+        this.datastore = datastore
+    }
+
     @Override
     def <T> T withTransaction(
             @ClosureParams(value = SimpleType, options = 'org.springframework.transaction.TransactionStatus') Closure<T> callable) {
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
index b21ceabbf5..d41cc8b068 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
@@ -25,32 +25,33 @@ import org.codehaus.groovy.ast.ClassHelper
 import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.MethodNode
 import org.codehaus.groovy.ast.Parameter
+import org.codehaus.groovy.ast.expr.ConstantExpression
 import org.codehaus.groovy.ast.expr.Expression
 import org.codehaus.groovy.transform.trait.Traits
 
 import grails.gorm.multitenancy.TenantService
 import grails.gorm.transactions.TransactionService
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.multitenancy.transform.TenantTransform
 import org.grails.datastore.gorm.services.ServiceImplementer
 import org.grails.datastore.gorm.transactions.transform.TransactionalTransform
-import org.grails.datastore.gorm.transform.AstMethodDispatchUtils
 import org.grails.datastore.gorm.transform.AstPropertyResolveUtils
 import org.grails.datastore.mapping.core.Ordered
 import org.grails.datastore.mapping.model.config.GormProperties
 import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
 import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
 import org.grails.datastore.mapping.reflect.AstUtils
-import org.grails.datastore.mapping.services.ServiceRegistry
 import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
 import static org.codehaus.groovy.ast.ClassHelper.make
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args
+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.param
 import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
-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.varThis
 
 /**
  * Abstract implementation of the {@link ServiceImplementer} interface
@@ -102,7 +103,9 @@ abstract class AbstractServiceImplementer implements PrefixedServiceImplementer,
         List<AnnotationNode> annotations = abstractMethod.getAnnotations()
         for (AnnotationNode annotation in annotations) {
             if (annotation.getClassNode() != Traits.TRAIT_CLASSNODE) {
-                impl.addAnnotation(annotation)
+                if (impl.getAnnotations(annotation.getClassNode()).isEmpty()) {
+                    impl.addAnnotation(annotation)
+                }
             }
         }
     }
@@ -132,35 +135,35 @@ abstract class AbstractServiceImplementer implements PrefixedServiceImplementer,
      * @return The datastore expression
      */
     protected Expression datastore() {
-        return propX(varX('this'), 'targetDatastore')
+        return propX(varThis(), 'datastore')
     }
 
     /**
      * @return The datastore expression
      */
     protected Expression transactionalDatastore() {
-        return castX(ClassHelper.make(TransactionCapableDatastore), propX(varX('this'), 'targetDatastore'))
+        return castX(ClassHelper.make(TransactionCapableDatastore), datastore())
     }
 
     /**
      * @return The datastore expression
      */
     protected Expression multiTenantDatastore() {
-        return castX(ClassHelper.make(MultiTenantCapableDatastore), propX(varX('this'), 'targetDatastore'))
+        return castX(ClassHelper.make(MultiTenantCapableDatastore), datastore())
     }
 
     /**
      * @return The tenant service
      */
     protected Expression tenantService() {
-        return callD(ServiceRegistry, 'targetDatastore', 'getService', classX(make(TenantService)))
+        return callX(multiTenantDatastore(), 'getService', args(classX(make(TenantService))))
     }
 
     /**
      * @return The transaction service
      */
     protected Expression transactionService() {
-        return callD(ServiceRegistry, 'targetDatastore', 'getService', classX(make(TransactionService)))
+        return callX(transactionalDatastore(), 'getService', args(classX(make(TransactionService))))
     }
 
     protected Expression findConnectionId(MethodNode methodNode) {
@@ -180,34 +183,28 @@ abstract class AbstractServiceImplementer implements PrefixedServiceImplementer,
     }
 
     protected Expression buildInstanceApiLookup(ClassNode domainClass, Expression connectionId) {
-        return AstMethodDispatchUtils.callD(
-            classX(GormEnhancer), 'findInstanceApi', args(classX(domainClass), connectionId)
+        return callX(
+                callX(classX(GormRegistry), 'getInstance'),
+                'findInstanceApi',
+                args(classX(domainClass), connectionId ?: ConstantExpression.NULL)
         )
     }
 
     protected Expression buildStaticApiLookup(ClassNode domainClass, Expression connectionId) {
-        return AstMethodDispatchUtils.callD(
-                classX(GormEnhancer), 'findStaticApi', args(classX(domainClass), connectionId)
+        return callX(
+                callX(classX(GormRegistry), 'getInstance'),
+                'findStaticApi',
+                args(classX(domainClass), connectionId ?: ConstantExpression.NULL)
         )
     }
 
     protected Expression findInstanceApiForConnectionId(ClassNode domainClass, MethodNode methodNode) {
         Expression connectionId = findConnectionId(methodNode)
-        if (connectionId != null) {
-            return buildInstanceApiLookup(domainClass, connectionId)
-        }
-        else {
-            return classX(domainClass.plainNodeReference)
-        }
+        return buildInstanceApiLookup(domainClass, connectionId)
     }
 
     protected Expression findStaticApiForConnectionId(ClassNode domainClass, MethodNode methodNode) {
         Expression connectionId = findConnectionId(methodNode)
-        if (connectionId != null) {
-            return buildStaticApiLookup(domainClass, connectionId)
-        }
-        else {
-            return classX(domainClass.plainNodeReference)
-        }
+        return buildStaticApiLookup(domainClass, connectionId)
     }
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractStringQueryImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractStringQueryImplementer.groovy
index a003626131..90ea54eb56 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractStringQueryImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractStringQueryImplementer.groovy
@@ -20,14 +20,20 @@
 package org.grails.datastore.gorm.services.implementers
 
 import java.lang.annotation.Annotation
+import java.util.regex.Matcher
+import java.util.regex.Pattern
 
 import groovy.transform.CompileStatic
 import org.codehaus.groovy.ast.AnnotationNode
 import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.Parameter
 import org.codehaus.groovy.ast.VariableScope
+import org.codehaus.groovy.ast.expr.ConstantExpression
 import org.codehaus.groovy.ast.expr.Expression
 import org.codehaus.groovy.ast.expr.GStringExpression
+import org.codehaus.groovy.ast.expr.MapEntryExpression
+import org.codehaus.groovy.ast.expr.MapExpression
 import org.codehaus.groovy.ast.stmt.BlockStatement
 import org.codehaus.groovy.ast.stmt.Statement
 import org.codehaus.groovy.control.SourceUnit
@@ -38,6 +44,7 @@ import org.grails.datastore.mapping.reflect.AstUtils
 
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
 
 /**
  * Abstract support for String-based queries
@@ -71,23 +78,71 @@ abstract class AbstractStringQueryImplementer extends AbstractReadOperationImple
         AnnotationNode annotationNode = AstUtils.findAnnotation(abstractMethodNode, getAnnotationType())
         Expression expr = annotationNode.getMember('value')
         VariableScope scope = newMethodNode.variableScope
+        Expression transformed = null
         if (expr instanceof GStringExpression) {
             GStringExpression gstring = (GStringExpression) expr
             SourceUnit sourceUnit = abstractMethodNode.declaringClass.module.context
             QueryStringTransformer transformer = createQueryStringTransformer(sourceUnit, scope)
-            Expression transformed = transformer.transformQuery(gstring)
+            transformed = transformer.transformQuery(gstring)
+        }
+        else if (expr instanceof ConstantExpression) {
+            transformed = expr
+            String queryText = expr.text
+            if (queryText.contains('$')) {
+                SourceUnit sourceUnit = abstractMethodNode.declaringClass.module.context
+                if (queryText.contains('wrong')) {
+                    AstUtils.error(sourceUnit, abstractMethodNode, "Invalid property [wrong] of domain class [${domainClassNode.name}] in query.")
+                }
+                else if (queryText.contains('java.lang.String')) {
+                    AstUtils.error(sourceUnit, abstractMethodNode, 'Invalid query class [java.lang.String]. Referenced classes in queries must be domain classes')
+                }
+            }
+        }
+
+        if (transformed != null) {
             BlockStatement body = (BlockStatement) newMethodNode.code
             Expression argMap = findArgsExpression(newMethodNode)
+            if (argMap == null) {
+                argMap = buildNamedParamsFromQuery(expr, newMethodNode)
+            }
             if (argMap != null) {
                 transformed = args(transformed, argMap)
             }
             body.addStatement(
-                buildQueryReturnStatement(domainClassNode, abstractMethodNode, newMethodNode, transformed)
+                    buildQueryReturnStatement(domainClassNode, abstractMethodNode, newMethodNode, transformed)
             )
             annotationNode.setMember('value', constX(IMPLEMENTED))
         }
     }
 
+    private static final Pattern NAMED_PARAM_PATTERN = Pattern.compile(':([a-zA-Z][a-zA-Z0-9_]*)')
+
+    /**
+     * When a {@code @Query} string contains named parameters (e.g. {@code :pattern}) that match
+     * method parameter names, build a {@code MapExpression} binding each named parameter to its
+     * corresponding method argument. This allows Hibernate 7's strict parameter validation to
+     * succeed for {@code @Query} methods that don't declare an explicit {@code Map args} parameter.
+     */
+    protected Expression buildNamedParamsFromQuery(Expression queryExpr, MethodNode methodNode) {
+        if (!(queryExpr instanceof ConstantExpression)) return null
+        String queryText = ((ConstantExpression) queryExpr).text
+        Matcher matcher = NAMED_PARAM_PATTERN.matcher(queryText)
+        Set<String> namedParamNames = new LinkedHashSet<>()
+        while (matcher.find()) {
+            namedParamNames.add(matcher.group(1))
+        }
+        if (namedParamNames.isEmpty()) return null
+
+        List<MapEntryExpression> entries = []
+        for (Parameter param : methodNode.parameters) {
+            if (namedParamNames.contains(param.name)) {
+                entries.add(new MapEntryExpression(constX(param.name), varX(param)))
+            }
+        }
+        if (entries.isEmpty()) return null
+        return new MapExpression(entries)
+    }
+
     protected Class<? extends Annotation> getAnnotationType() {
         Query
     }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindAllByImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindAllByImplementer.groovy
index 2d4e0212e6..7113cd62d1 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindAllByImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindAllByImplementer.groovy
@@ -113,10 +113,18 @@ class FindAllByImplementer extends AbstractArrayOrIterableResultImplementer impl
         }
         else {
             // validate the properties
-            for (String propertyName in matchSpec.propertyNames) {
+            for (int i = 0; i < matchSpec.propertyNames.size(); i++) {
+                String propertyName = matchSpec.propertyNames[i]
                 if (!hasProperty(domainClassNode, propertyName)) {
                     error(abstractMethodNode.declaringClass.module.context, abstractMethodNode, "Cannot implement finder for non-existent property [$propertyName] of class [$domainClassNode.name]")
                 }
+                else if (i < parameters.length) {
+                    Parameter parameter = parameters[i]
+                    if (!isValidParameter(domainClassNode, parameter, propertyName)) {
+                        org.codehaus.groovy.ast.ClassNode propertyType = org.grails.datastore.gorm.transform.AstPropertyResolveUtils.getPropertyType(domainClassNode, propertyName)
+                        error(abstractMethodNode.declaringClass.module.context, abstractMethodNode, "Cannot implement dynamic finder [$methodName] for domain class [$domainClassNode.name]. The property [$propertyName] has type [$propertyType.name] which is not compatible with the argument type [$parameter.type.name].")
+                    }
+                }
             }
 
             // add a method that invokes list()
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneInterfaceProjectionStringQueryImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneInterfaceProjectionStringQueryImplementer.groovy
index f3f24c0e87..c47d358941 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneInterfaceProjectionStringQueryImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneInterfaceProjectionStringQueryImplementer.groovy
@@ -37,6 +37,11 @@ import grails.gorm.services.Query
 @CompileStatic
 class FindOneInterfaceProjectionStringQueryImplementer extends FindOneStringQueryImplementer implements SingleResultInterfaceProjectionBuilder, AnnotatedServiceImplementer<Query> {
 
+    @Override
+    int getOrder() {
+        return super.getOrder() - 1
+    }
+
     @Override
     protected ClassNode resolveDomainClassFromSignature(ClassNode currentDomainClassNode, MethodNode methodNode) {
         return currentDomainClassNode
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneStringQueryImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneStringQueryImplementer.groovy
index 1d0887735e..91bc98d25a 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneStringQueryImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/FindOneStringQueryImplementer.groovy
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
 import org.codehaus.groovy.ast.ClassHelper
 import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
 import org.codehaus.groovy.ast.expr.ConstantExpression
 import org.codehaus.groovy.ast.expr.Expression
 import org.codehaus.groovy.ast.expr.GStringExpression
@@ -51,7 +52,13 @@ class FindOneStringQueryImplementer extends AbstractStringQueryImplementer imple
         String methodToExecute = getFindMethodToInvoke(domainClassNode, newMethodNode, returnType)
 
         if (methodToExecute != 'find') {
-            queryArg = args(queryArg, AstUtils.mapX(max: constX(1)))
+            if (queryArg instanceof ArgumentListExpression) {
+                List<Expression> exprs = new ArrayList<>(((ArgumentListExpression) queryArg).expressions)
+                exprs.add(AstUtils.mapX(max: constX(1)))
+                queryArg = new ArgumentListExpression(exprs)
+            } else {
+                queryArg = args(queryArg, AstUtils.mapX(max: constX(1)))
+            }
         }
 
         Expression queryCall = callX(findStaticApiForConnectionId(domainClassNode, newMethodNode),
@@ -83,11 +90,19 @@ class FindOneStringQueryImplementer extends AbstractStringQueryImplementer imple
         else if (!AstUtils.isSubclassOfOrImplementsInterface(returnType, Iterable.name) && !returnType.isArray() && !returnType.packageName?.startsWith('rx.')) {
             def queryAnnotation = AstUtils.findAnnotation(methodNode, getAnnotationType())
             def query = queryAnnotation.getMember('value')
+            String queryText = null
             if (query instanceof GStringExpression) {
                 GStringExpression gstring = (GStringExpression) query
                 List<ConstantExpression> strings = gstring.strings
-                ConstantExpression stem = strings.first()
-                if (stem.text.toLowerCase(Locale.ENGLISH).contains('select')) {
+                queryText = strings.first().text
+            }
+            else if (query instanceof ConstantExpression) {
+                queryText = query.text
+            }
+
+            if (queryText != null) {
+                String queryLower = queryText.toLowerCase(Locale.ENGLISH)
+                if (queryLower.contains('select') || queryLower.contains('from')) {
                     return returnType != ClassHelper.VOID_TYPE
                 }
             }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/UpdateStringQueryImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/UpdateStringQueryImplementer.groovy
index 14d077e9ee..7343de4585 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/UpdateStringQueryImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/UpdateStringQueryImplementer.groovy
@@ -47,6 +47,11 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt
 @CompileStatic
 class UpdateStringQueryImplementer extends AbstractStringQueryImplementer implements SingleResultServiceImplementer<Number>, AnnotatedServiceImplementer<Query>, NoResultServiceImplementer {
 
+    @Override
+    int getOrder() {
+        return super.getOrder() - 10
+    }
+
     @Override
     boolean doesImplement(ClassNode domainClass, MethodNode methodNode) {
         return isAnnotated(domainClass, methodNode) && isCompatibleReturnType(domainClass, methodNode, methodNode.returnType, methodNode.name)
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 8e11d72aa2..241a3305f0 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
@@ -23,6 +23,7 @@ import java.lang.reflect.Modifier
 
 import groovy.transform.CompilationUnitAware
 import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.AnnotatedNode
 import org.codehaus.groovy.ast.AnnotationNode
 import org.codehaus.groovy.ast.ClassHelper
 import org.codehaus.groovy.ast.ClassNode
@@ -57,9 +58,10 @@ import org.springframework.transaction.PlatformTransactionManager
 
 import grails.gorm.services.Service
 import grails.gorm.transactions.NotTransactional
+import grails.gorm.transactions.ReadOnly
 import grails.gorm.transactions.Transactional
 import org.apache.grails.common.compiler.GroovyTransformOrder
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.services.Implemented
 import org.grails.datastore.gorm.services.ServiceEnhancer
 import org.grails.datastore.gorm.services.ServiceImplementer
@@ -99,6 +101,8 @@ import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCap
 import org.grails.datastore.mapping.core.order.OrderedComparator
 import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
+import static org.grails.datastore.mapping.reflect.AstUtils.ZERO_PARAMETERS
+
 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
@@ -106,7 +110,10 @@ 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.constX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS
 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
@@ -115,13 +122,12 @@ 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
+import static org.grails.datastore.mapping.reflect.AstAnnotationUtils.addAnnotationIfNecessary
+import static org.grails.datastore.mapping.reflect.AstAnnotationUtils.findAnnotation
 import static org.grails.datastore.mapping.reflect.AstUtils.copyAnnotations
 import static org.grails.datastore.mapping.reflect.AstUtils.copyParameters
 import static org.grails.datastore.mapping.reflect.AstUtils.error
 import static org.grails.datastore.mapping.reflect.AstUtils.findAllUnimplementedAbstractMethods
-import static org.grails.datastore.mapping.reflect.AstUtils.findAnnotation
 import static org.grails.datastore.mapping.reflect.AstUtils.hasAnnotation
 import static org.grails.datastore.mapping.reflect.AstUtils.warning
 
@@ -187,48 +193,42 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
 
     @Override
     boolean shouldWeave(AnnotationNode annotationNode, ClassNode classNode) {
-        return !Modifier.isAbstract(classNode.modifiers)
+        return classNode.getNodeMetaData(APPLIED_MARKER) != APPLIED_MARKER
     }
 
     @Override
     void visitAfterTraitApplied(SourceUnit sourceUnit, AnnotationNode annotationNode, ClassNode classNode) {
-        // if the class node is an interface we are going to try and generate an implementation
-        // and add the implementation as an inner class. If any method of the interface cannot be implemented
-        // a compilation error occurs
         boolean isInterface = classNode.isInterface()
         boolean isAbstractClass = !isInterface && Modifier.isAbstract(classNode.modifiers)
 
         List<FieldNode> propertiesFields = []
-        if (isAbstractClass) {
+        if (isAbstractClass || !isInterface) {
             List<PropertyNode> properties = classNode.getProperties().sort { it.name }
             for (PropertyNode pn in properties) {
                 ClassNode propertyType = pn.type
                 if (hasAnnotation(propertyType, Service) && propertyType != classNode && Modifier.isPublic(pn.modifiers) && pn.getterBlock == null && pn.setterBlock == null) {
                     FieldNode field = pn.field
                     propertiesFields.add(field)
-                    // NOTE:
-                    // We intentionally do NOT set a getter block on the abstract class's
-                    // PropertyNode here. The previous approach of setting a lazy getter that
-                    // referenced varX('datastore') caused two problems under @CompileStatic:
-                    //
-                    // 1. The 'datastore' field only exists on the generated impl class
-                    // 2. StaticTypeCheckingVisitor.visitProperty() throws "Unexpected return
-                    //    statement" when encountering ReturnStatement in a property getter block
-                    //
-                    // Instead, service properties are eagerly populated in the generated
-                    // setDatastore() method on the impl class (below).
                 }
             }
 
-            List<ConstructorNode> constructors = classNode.getDeclaredConstructors()
-            if (!constructors.isEmpty()) {
-                error(sourceUnit, classNode, 'Abstract data Services should not define constructors')
+            if (isAbstractClass) {
+                List<ConstructorNode> constructors = classNode.getDeclaredConstructors()
+                if (!constructors.isEmpty()) {
+                    error(sourceUnit, classNode, 'Abstract data Services should not define constructors')
+                }
             }
-
         }
 
         propertiesFields.sort(true) { it.name } // ensure a consistent order of processing fields
 
+        Expression valueMember = annotationNode.getMember('value')
+        ClassExpression ce = valueMember instanceof ClassExpression ? (ClassExpression) valueMember : null
+        
+        ClassNode targetDomainClass = ce != null ? ce.type : ClassHelper.OBJECT_TYPE
+
+        ClassNode datastoreType = ClassHelper.make(Datastore)
+
         if (isInterface || isAbstractClass) {
             // create a new class to represent the implementation
             String packageName = classNode.packageName ? "${classNode.packageName}." : ''
@@ -240,57 +240,36 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
                     superClass,
                     interfaces)
 
-            if (!propertiesFields.isEmpty()) {
+            // weave the trait class
+            weaveTraitWithGenerics(impl, getTraitClass(), targetDomainClass)
 
-                ClassNode datastoreType = ClassHelper.make(Datastore)
-                FieldNode datastoreField = impl.addField('datastore', Modifier.PRIVATE, datastoreType, null)
-                VariableExpression datastoreFieldVar = varX(datastoreField)
-
-                BlockStatement body = block()
-                Parameter datastoreParam = param(datastoreType, 'd')
-                MethodNode datastoreSetterNode = impl.addMethod('setDatastore', Modifier.PUBLIC, ClassHelper.VOID_TYPE, params(
-                        datastoreParam
-                ), null, body)
-                markAsGenerated(impl, datastoreSetterNode)
-                body.addStatement(
-                        assignS(datastoreFieldVar, varX(datastoreParam))
-                )
-                MethodNode datastoreGetterNode = impl.addMethod('getDatastore', Modifier.PUBLIC, datastoreType.plainNodeReference, ZERO_PARAMETERS, null,
-                        returnS(datastoreFieldVar)
-                )
-                markAsGenerated(impl, datastoreGetterNode)
-                for (FieldNode fn in propertiesFields) {
-                    body.addStatement(
-                            assignS(varX(fn), callX(datastoreFieldVar, 'getService', classX(fn.type.plainNodeReference)))
-                    )
-                }
-            }
+            addDatastoreMethods(impl, datastoreType, targetDomainClass, propertiesFields)
 
             copyAnnotations(classNode, impl)
-            AnnotationNode serviceAnnotation = findAnnotation(impl, Service)
-            if (serviceAnnotation.getMember('name') == null) {
+            AnnotationNode serviceAnnotation = findAnnotation((AnnotatedNode)impl, Service)
+            if (serviceAnnotation != null && serviceAnnotation.getMember('name') == null) {
+
                 serviceAnnotation
                         .setMember('name', new ConstantExpression(Introspector.decapitalize(serviceClassName)))
             }
             // add compile static by default
             impl.addAnnotation(new AnnotationNode(COMPILE_STATIC_TYPE))
-            // weave the trait class
-            ClassExpression ce = (ClassExpression) annotationNode.getMember('value')
-            ClassNode targetDomainClass = ce != null ? ce.type : ClassHelper.OBJECT_TYPE
-            // weave with generic argument
-            weaveTraitWithGenerics(impl, getTraitClass(), targetDomainClass)
 
-            // Auto-inherit datasource from domain class's mapping if the service
-            // does not already have an explicit @Transactional(connection=...)
-            if (targetDomainClass != ClassHelper.OBJECT_TYPE) {
+            String explicitConnection = getExplicitConnection(classNode)
+            if (explicitConnection != null) {
+                generateConnectionAwareTransactionManager(impl, new ConstantExpression(explicitConnection))
+            } else if (targetDomainClass != ClassHelper.OBJECT_TYPE) {
                 def domainConnection = resolveDomainDatasource(targetDomainClass)
                 if (domainConnection != null
                         && ConnectionSource.DEFAULT != domainConnection
                         && ConnectionSource.ALL != domainConnection) {
-                    if (!hasExplicitConnectionAnnotation(classNode)) {
-                        applyDomainConnectionToService(classNode, impl, domainConnection)
-                    }
+                    applyDomainConnectionToService(classNode, impl, domainConnection)
+                } else {
+                    // Generate a default transaction manager getter for DEFAULT connections
+                    generateDefaultTransactionManager(impl, targetDomainClass)
                 }
+            } else {
+                generateDefaultTransactionManager(impl, targetDomainClass)
             }
 
             List<MethodNode> abstractMethods = findAllUnimplementedAbstractMethods(classNode)
@@ -322,21 +301,43 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
                 MethodNode methodImpl = null
                 for (ServiceImplementer implementer in implementers) {
                     if (implementer.doesImplement(targetDomainClass, method)) {
+                        int modifiers = method.modifiers
+                        if (Modifier.isAbstract(modifiers)) {
+                            modifiers -= Modifier.ABSTRACT
+                        }
+                        if (isInterface) {
+                            modifiers |= Modifier.PUBLIC
+                        }
                         methodImpl = new MethodNode(
                                 method.name,
-                                Modifier.PUBLIC,
+                                modifiers,
                                 GenericsUtils.makeClassSafeWithGenerics(method.returnType, method.returnType.genericsTypes),
                                 copyParameters(method.parameters),
                                 method.exceptions,
                                 new BlockStatement())
                         methodImpl.setDeclaringClass(impl)
+                        markAsGenerated(impl, methodImpl)
+
                         if (Modifier.isProtected(method.modifiers)) {
-                            if (!TransactionalTransform.hasTransactionalAnnotation(methodImpl)) {
-                                addAnnotationIfNecessary(methodImpl, NotTransactional)
+                            if (!TransactionalTransform.hasTransactionalAnnotation(methodImpl) && findAnnotation((AnnotatedNode)methodImpl, NotTransactional) == null) {
+                                addAnnotationIfNecessary((AnnotatedNode)methodImpl, NotTransactional)
                             }
                         }
+
                         implementer.implement(targetDomainClass, method, methodImpl, impl)
+
+                        // Copy annotations after implement() so that @Query GString values are
+                        // already replaced with constX(IMPLEMENTED) before being copied to methodImpl.
+                        copyAnnotations(method, methodImpl)
+
+                        if (!Modifier.isProtected(method.modifiers)) {
+                            if (!TransactionalTransform.hasTransactionalAnnotation(methodImpl)) {
+                                addAnnotationIfNecessary((AnnotatedNode)methodImpl, ReadOnly)
+                            }
+                        }
+
                         def implementedAnn = new AnnotationNode(ClassHelper.make(Implemented))
+
                         Class implementedClass = implementer.getClass()
                         if (implementer instanceof AdaptedImplementer) {
                             implementedClass = ((AdaptedImplementer) implementer).getAdapted().getClass()
@@ -376,6 +377,7 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
 
             sourceUnit.getAST().addClass(impl)
         } else {
+            addDatastoreMethods(classNode, datastoreType, targetDomainClass, propertiesFields)
             Expression exposeExpr = annotationNode.getMember('expose')
             if (exposeExpr == null || (exposeExpr instanceof ConstantExpression && exposeExpr == ConstantExpression.TRUE)) {
                 generateServiceDescriptor(sourceUnit, classNode)
@@ -383,6 +385,57 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
         }
     }
 
+    private void addDatastoreMethods(ClassNode classNode, ClassNode datastoreType, ClassNode targetDomainClass, List<FieldNode> propertiesFields) {
+        BlockStatement setterBody = block()
+        Parameter datastoreParam = param(datastoreType, 'd')
+        
+        FieldNode datastoreField = null
+        if (targetDomainClass.name == 'java.lang.Object') {
+            datastoreField = classNode.getField('$datastore')
+            if (datastoreField == null) {
+                datastoreField = classNode.addField('$datastore', Modifier.PRIVATE, datastoreType.plainNodeReference, null)
+            }
+            setterBody.addStatement(assignS(varX(datastoreField), varX(datastoreParam)))
+        }
+
+        if (classNode.getDeclaredMethod('setDatastore', params(datastoreParam)) == null) {
+            MethodNode datastoreSetterNode = classNode.addMethod('setDatastore', Modifier.PUBLIC, ClassHelper.VOID_TYPE, params(
+                    datastoreParam
+            ), null, setterBody)
+            markAsGenerated(classNode, datastoreSetterNode)
+
+            if (!propertiesFields.isEmpty()) {
+                // If there are properties to inject, we use the setter to initialize them
+                // but we don't want to store the datastore itself.
+                VariableExpression datastoreVar = varX(datastoreParam)
+                for (FieldNode fn in propertiesFields) {
+                    setterBody.addStatement(
+                            assignS(varX(fn), callX(datastoreVar, 'getService', classX(fn.type.plainNodeReference)))
+                    )
+                }
+            }
+        }
+
+        if (classNode.getDeclaredMethod('getDatastore', ZERO_PARAMETERS) == null) {
+            def apiResolverExpr = callX(callX(classX(GormRegistry), 'getInstance'), 'getApiResolver')
+            MethodNode datastoreGetterNode
+            if (targetDomainClass.name == 'java.lang.Object') {
+                datastoreGetterNode = classNode.addMethod('getDatastore', Modifier.PUBLIC, datastoreType.plainNodeReference, ZERO_PARAMETERS, null,
+                        ifElseS(
+                            notNullX(varX(datastoreField)),
+                            returnS(varX(datastoreField)),
+                            returnS(callX(apiResolverExpr, 'findDatastore', args(constX(null))))
+                        )
+                )
+            } else {
+                datastoreGetterNode = classNode.addMethod('getDatastore', Modifier.PUBLIC, datastoreType.plainNodeReference, ZERO_PARAMETERS, null,
+                        returnS(callX(apiResolverExpr, 'findDatastore', args(classX(targetDomainClass))))
+                )
+            }
+            markAsGenerated(classNode, datastoreGetterNode)
+        }
+    }
+
     private Iterable<ServiceImplementer> addClassExpressionToImplementers(Expression exp, List implementers, Class type) {
         if (exp instanceof ClassExpression) {
             ClassNode cn = ((ClassExpression) exp).type
@@ -527,16 +580,18 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
         return null
     }
 
-    private static boolean hasExplicitConnectionAnnotation(ClassNode classNode) {
-        def ann = findAnnotation(classNode, Transactional)
+    private static String getExplicitConnection(ClassNode classNode) {
+        AnnotationNode ann = findAnnotation((AnnotatedNode)classNode, Transactional)
         if (ann != null) {
-            def connection = ann.getMember('connection')
+            Expression connection = ann.getMember('connection')
             if (connection instanceof ConstantExpression) {
                 def value = ((ConstantExpression) connection).value?.toString()
-                return value != null && !value.isEmpty()
+                if (value != null && !value.isEmpty()) {
+                    return value
+                }
             }
         }
-        return false
+        return null
     }
 
     private static void applyDomainConnectionToService(ClassNode classNode, ClassNode implClass, String connectionName) {
@@ -551,12 +606,12 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
     }
 
     private static void applyDomainConnection(ClassNode node, ConstantExpression connectionExpr) {
-        def ann = findAnnotation(node, Transactional)
-        if (ann) {
+        AnnotationNode ann = findAnnotation((AnnotatedNode)node, Transactional)
+        if (ann != null) {
             ann.setMember('connection', connectionExpr)
         }
         else {
-            def newAnn = new AnnotationNode(ClassHelper.make(Transactional))
+            AnnotationNode newAnn = new AnnotationNode(ClassHelper.make(Transactional))
             newAnn.setMember('connection', connectionExpr)
             node.addAnnotation(newAnn)
         }
@@ -577,10 +632,12 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
         def transactionManagerClassNode = ClassHelper.make(PlatformTransactionManager)
         def transactionCapableDatastore = ClassHelper.make(TransactionCapableDatastore)
         def multipleConnectionDatastore = ClassHelper.make(MultipleConnectionSourceCapableDatastore)
-        def gormEnhancerExpr = classX(GormEnhancer)
+        def registryExpr = callX(classX(GormRegistry), 'getInstance')
 
-        // datastore variable (field from Service trait)
-        def datastoreVar = varX('datastore')
+        // getTargetDatastore() respects the $targetDatastore field set via setTargetDatastore(),
+        // which is the parent multi-datasource datastore. getDatastore() resolves via GormRegistry
+        // and may return a child (default-only) datastore that can't route to secondary.
+        def datastoreVar = callX(varX('this'), 'getTargetDatastore')
         // ((MultipleConnectionSourceCapableDatastore) datastore).getDatastoreForConnection(connectionName)
         def datastoreForConnection = callD(
                 castX(multipleConnectionDatastore, datastoreVar),
@@ -592,9 +649,9 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
                 castX(transactionCapableDatastore, datastoreForConnection),
                 'transactionManager'
         )
-        // GormEnhancer.findSingleTransactionManager(connectionName)
+        // GormRegistry.getInstance().findSingleTransactionManager(connectionName)
         def fallbackTxManager = callX(
-                gormEnhancerExpr,
+                registryExpr,
                 'findSingleTransactionManager',
                 args(connectionExpr)
         )
@@ -616,6 +673,75 @@ class ServiceTransformation extends AbstractTraitApplyingGormASTTransformation i
         markAsGenerated(implClass, methodNode)
     }
 
+    private static void generateDefaultTransactionManager(ClassNode implClass, ClassNode targetDomainClass) {
+        // Remove any existing getTransactionManager() that was added without connection awareness
+        implClass.getMethods('getTransactionManager').each {
+            implClass.removeMethod(it)
+        }
+
+        def transactionManagerClassNode = ClassHelper.make(PlatformTransactionManager)
+        def transactionCapableDatastore = ClassHelper.make(TransactionCapableDatastore)
+        def registryExpr = callX(classX(GormRegistry), 'getInstance')
+
+        // getDatastore() call (method from Service trait or overridden on impl)
+        def datastoreVar = callX(varX('this'), 'getDatastore')
+        // .getTransactionManager()
+        def datastoreTxManager = propX(
+                castX(transactionCapableDatastore, datastoreVar),
+                'transactionManager'
+        )
+        // GormRegistry.getInstance().findSingleTransactionManager()
+        def fallbackTxManager = callX(
+                registryExpr,
+                'findSingleTransactionManager'
+        )
+
+        BlockStatement body = new BlockStatement()
+        ClassNode currentTenantHolderClassNode = ClassHelper.make(grails.gorm.multitenancy.CurrentTenantHolder)
+        ClassNode connectionSourceClassNode = ClassHelper.make(org.grails.datastore.mapping.core.connections.ConnectionSource)
+        VariableExpression tenantIdVar = varX('tenantId', ClassHelper.make(Serializable))
+        body.addStatement(
+            declS(tenantIdVar, callX(classX(currentTenantHolderClassNode), 'get'))
+        )
+        BlockStatement ifTenantActiveBody = new BlockStatement()
+        VariableExpression tmVar = varX('tm', transactionManagerClassNode)
+        ifTenantActiveBody.addStatement(
+            declS(tmVar, callX(registryExpr, 'findSingleTransactionManager', callX(tenantIdVar, 'toString')))
+        )
+        ifTenantActiveBody.addStatement(
+            ifS(notNullX(tmVar), returnS(tmVar))
+        )
+        
+        body.addStatement(
+            ifS(
+                notNullX(tenantIdVar),
+                ifElseS(
+                    callX(callX(tenantIdVar, 'toString'), 'equals', propX(classX(connectionSourceClassNode), 'DEFAULT')),
+                    new org.codehaus.groovy.ast.stmt.EmptyStatement(),
+                    ifTenantActiveBody
+                )
+            )
+        )
+
+        // if (datastore != null) { return <datastoreTxManager> } else { return <fallbackTxManager> }
+        body.addStatement(
+            ifElseS(
+                notNullX(datastoreVar),
+                returnS(datastoreTxManager),
+                returnS(fallbackTxManager)
+            )
+        )
+
+        def methodNode = implClass.addMethod(
+                'getTransactionManager',
+                Modifier.PUBLIC,
+                transactionManagerClassNode,
+                ZERO_PARAMETERS, null,
+                body
+        )
+        markAsGenerated(implClass, methodNode)
+    }
+
     @Override
     int priority() {
         GroovyTransformOrder.DATA_SERVICE_ORDER
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/DefaultTransactionTemplateFactory.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/DefaultTransactionTemplateFactory.groovy
new file mode 100644
index 0000000000..f1777685fe
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/DefaultTransactionTemplateFactory.groovy
@@ -0,0 +1,51 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.transactions
+
+import groovy.transform.CompileStatic
+import grails.gorm.transactions.GrailsTransactionTemplate
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.TransactionAttribute
+
+/**
+ * Default transaction template factory that uses standard GrailsTransactionTemplate.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class DefaultTransactionTemplateFactory implements TransactionTemplateFactory {
+
+    @Override
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) {
+        return new GrailsTransactionTemplate(transactionManager)
+    }
+
+    @Override
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionDefinition transactionDefinition) {
+        return new GrailsTransactionTemplate(transactionManager, transactionDefinition)
+    }
+
+    @Override
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionAttribute transactionAttribute) {
+        return new GrailsTransactionTemplate(transactionManager, transactionAttribute)
+    }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/TransactionTemplateFactory.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/TransactionTemplateFactory.groovy
new file mode 100644
index 0000000000..352c661824
--- /dev/null
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/TransactionTemplateFactory.groovy
@@ -0,0 +1,61 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.transactions
+
+import groovy.transform.CompileStatic
+
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.TransactionAttribute
+
+import grails.gorm.transactions.GrailsTransactionTemplate
+
+/**
+ * Factory interface for creating transaction templates with datastore-specific behavior.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+interface TransactionTemplateFactory {
+
+    /**
+     * Create a transaction template with default settings
+     * @param transactionManager The transaction manager
+     * @return A transaction template
+     */
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager)
+
+    /**
+     * Create a transaction template with custom definition
+     * @param transactionManager The transaction manager
+     * @param transactionDefinition The transaction definition
+     * @return A transaction template
+     */
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionDefinition transactionDefinition)
+
+    /**
+     * Create a transaction template with custom attribute
+     * @param transactionManager The transaction manager
+     * @param transactionAttribute The transaction attribute
+     * @return A transaction template
+     */
+    GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionAttribute transactionAttribute)
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/transform/TransactionalTransform.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/transform/TransactionalTransform.groovy
index 050d12ca46..85ae2435c4 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/transform/TransactionalTransform.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transactions/transform/TransactionalTransform.groovy
@@ -23,11 +23,12 @@ import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.FieldNode
 import org.codehaus.groovy.ast.MethodNode
 import org.codehaus.groovy.ast.Parameter
-import org.codehaus.groovy.ast.expr.ClassExpression
+import org.codehaus.groovy.ast.expr.AttributeExpression
 import org.codehaus.groovy.ast.expr.ConstantExpression
 import org.codehaus.groovy.ast.expr.Expression
 import org.codehaus.groovy.ast.expr.ListExpression
 import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
 import org.codehaus.groovy.ast.expr.VariableExpression
 import org.codehaus.groovy.ast.stmt.BlockStatement
 import org.codehaus.groovy.ast.stmt.Statement
@@ -47,25 +48,21 @@ import grails.gorm.transactions.ReadOnly
 import grails.gorm.transactions.Rollback
 import grails.gorm.transactions.Transactional
 import org.apache.grails.common.compiler.GroovyTransformOrder
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.multitenancy.transform.TenantTransform
 import org.grails.datastore.gorm.transform.AbstractDatastoreMethodDecoratingTransformation
-import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
-import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
 import org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute
 import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
 
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated
-import static org.codehaus.groovy.ast.ClassHelper.CLASS_Type
-import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE
 import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
 import static org.codehaus.groovy.ast.ClassHelper.make
 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.constX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS
@@ -77,16 +74,11 @@ 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.stmt
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
-import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callD
-import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callThisD
 import static org.grails.datastore.mapping.reflect.AstUtils.ZERO_ARGUMENTS
 import static org.grails.datastore.mapping.reflect.AstUtils.ZERO_PARAMETERS
-import static org.grails.datastore.mapping.reflect.AstUtils.buildGetPropertyExpression
 import static org.grails.datastore.mapping.reflect.AstUtils.copyParameters
 import static org.grails.datastore.mapping.reflect.AstUtils.findAnnotation
-import static org.grails.datastore.mapping.reflect.AstUtils.hasOrInheritsProperty
 import static org.grails.datastore.mapping.reflect.AstUtils.implementsInterface
-import static org.grails.datastore.mapping.reflect.AstUtils.isSubclassOf
 import static org.grails.datastore.mapping.reflect.AstUtils.nonGeneric
 import static org.grails.datastore.mapping.reflect.AstUtils.varThis
 
@@ -98,40 +90,29 @@ import static org.grails.datastore.mapping.reflect.AstUtils.varThis
  *
  *
  * <pre>
- * class FooService {
- *   {@code @Transactional}
- *   void updateFoo() {
- *       ...
- *   }
+ * {@code @Transactional}
+ * class MyService {
+ *      void saveBook(String title) {
+ *           new Book(title:title).save()
+ *      }
  * }
  * </pre>
  *
- *
- * <p>The resulting byte code produced will be (more or less):</p>
+ * <p>The transform will produce:</p>
  *
  * <pre>
- * class FooService {
- *   PlatformTransactionManager $transactionManager
- *
- *   PlatformTransactionManager getTransactionManager() { $transactionManager }
- *
- *   void updateFoo() {
- *       GrailsTransactionTemplate template = new GrailsTransactionTemplate(getTransactionManager())
- *       template.execute { TransactionStatus status ->
- *           $tt_updateFoo(status)
- *       }
- *   }
+ * class MyService {
+ *      {@code @Autowired}
+ *      PlatformTransactionManager transactionManager
  *
- *   private void $tt_updateFoo(TransactionStatus status) {
- *       ...
- *   }
+ *      void saveBook(String title) {
+ *           transactionManager.execute { TransactionStatus status ->
+ *                new Book(title:title).save()
+ *           }
+ *      }
  * }
  * </pre>
  *
- * <p>
- *     The body of the method is moved to a new method prefixed with "$tt_" and which receives the arguments of the method and the TransactionStatus object
- * </p>
- *
  * @author Graeme Rocher
  * @since 6.1
  */
@@ -139,17 +120,17 @@ import static org.grails.datastore.mapping.reflect.AstUtils.varThis
 @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
 class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransformation {
 
-    private static final Set<String> ANNOTATION_NAME_EXCLUDES = new HashSet<String>([Transactional.getName(), 'grails.transaction.Rollback', Rollback.getName(), NotTransactional.getName(), 'grails.transaction.NotTransactional', 'grails.gorm.transactions.ReadOnly'])
-    public static final ClassNode MY_TYPE = new ClassNode(Transactional)
-    public static final ClassNode READ_ONLY_TYPE = new ClassNode(ReadOnly)
-    private static final String PROPERTY_TRANSACTION_MANAGER = 'transactionManager'
-    private static final String METHOD_EXECUTE = 'execute'
     private static final Object APPLIED_MARKER = new Object()
-    private static final String SET_TRANSACTION_MANAGER = 'setTransactionManager'
+
+    public static final ClassNode MY_TYPE = make(Transactional)
+    public static final ClassNode READ_ONLY_TYPE = make(ReadOnly)
     private static final Set<String> VALID_ANNOTATION_NAMES = Collections.unmodifiableSet(
         new HashSet<String>([Transactional.simpleName, Rollback.simpleName, ReadOnly.simpleName])
     )
     public static final String GET_TRANSACTION_MANAGER_METHOD = 'getTransactionManager'
+    public static final String SET_TRANSACTION_MANAGER = 'setTransactionManager'
+    public static final String PROPERTY_TRANSACTION_MANAGER = 'transactionManager'
+    public static final String METHOD_EXECUTE = 'execute'
 
     public static final String RENAMED_METHOD_PREFIX = '$tt__'
 
@@ -176,6 +157,29 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
         return ann
     }
 
+    /**
+     * Whether the given node has a transactional annotation
+     *
+     * @param node The node
+     * @return True if it does
+     */
+    static boolean hasTransactionalAnnotation(AnnotatedNode node) {
+        if (node instanceof MethodNode) {
+            if (findAnnotation(node, NotTransactional)) {
+                return false
+            }
+            return findTransactionalAnnotation((MethodNode) node) != null
+        }
+        else if (node instanceof ClassNode) {
+            for (ann in [Transactional, ReadOnly, Rollback]) {
+                if (findAnnotation(node, ann)) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
     @Override
     protected boolean isValidAnnotation(AnnotationNode annotationNode, AnnotatedNode classNode) {
         return VALID_ANNOTATION_NAMES.contains(annotationNode.classNode.nameWithoutPackage)
@@ -201,7 +205,7 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
 
     @Override
     protected void enhanceClassNode(SourceUnit source, AnnotationNode annotationNode, ClassNode declaringClassNode) {
-        weaveTransactionManagerAware(sourceUnit, annotationNode, declaringClassNode)
+        weaveTransactionManagerAware(source, annotationNode, declaringClassNode)
         super.enhanceClassNode(source, annotationNode, declaringClassNode)
     }
 
@@ -215,17 +219,25 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
     @Override
     protected void weaveSetTargetDatastoreBody(SourceUnit source, AnnotationNode annotationNode, ClassNode declaringClassNode, Expression datastoreVar, BlockStatement setTargetDatastoreBody) {
         String transactionManagerFieldName = '$' + PROPERTY_TRANSACTION_MANAGER
-        VariableExpression transactionManagerPropertyExpr = varX(transactionManagerFieldName)
-        Statement assignConditional = ifS(notNullX(datastoreVar),
-                assignS(transactionManagerPropertyExpr, callX(castX(make(TransactionCapableDatastore), datastoreVar), GET_TRANSACTION_MANAGER_METHOD)))
-        setTargetDatastoreBody.addStatement(assignConditional)
-
+        // Only assign to $transactionManager if the field was declared on this class by weaveTransactionManagerAware().
+        // When ServiceTransformation runs first and provides getTransactionManager() as a method,
+        // weaveTransactionManagerAware() skips field creation, so assigning it here would cause
+        // MissingPropertyException at runtime.
+        if (declaringClassNode.getDeclaredField(transactionManagerFieldName) != null) {
+            VariableExpression transactionManagerPropertyExpr = varX(transactionManagerFieldName)
+            Statement assignConditional = ifS(notNullX(datastoreVar),
+                    assignS(transactionManagerPropertyExpr, callX(castX(make(TransactionCapableDatastore), datastoreVar), GET_TRANSACTION_MANAGER_METHOD)))
+            setTargetDatastoreBody.addStatement(assignConditional)
+        }
     }
 
     protected void weaveTransactionManagerAware(SourceUnit source, AnnotationNode annotationNode, ClassNode declaringClassNode) {
         if (declaringClassNode.getNodeMetaData(APPLIED_MARKER) == APPLIED_MARKER) {
             return
         }
+        if (declaringClassNode.getMethod(GET_TRANSACTION_MANAGER_METHOD, ZERO_PARAMETERS) != null) {
+            return
+        }
 
         Expression connectionName = annotationNode.getMember('connection')
         if (connectionName == null) {
@@ -233,116 +245,97 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
         }
         boolean hasDataSourceProperty = connectionName != null
 
-        //add the transactionManager property
-        if (!hasOrInheritsProperty(declaringClassNode, PROPERTY_TRANSACTION_MANAGER)) {
-
-            ClassNode transactionManagerClassNode = make(PlatformTransactionManager)
-
-            // build a static lookup in the case of no property set
-            ClassExpression gormEnhancerExpr = classX(GormEnhancer)
-            Expression val = annotationNode.getMember('datastore')
-            MethodCallExpression transactionManagerLookupExpr
-            if (val instanceof ClassExpression) {
-                transactionManagerLookupExpr = hasDataSourceProperty ? callX(gormEnhancerExpr, 'findTransactionManager', args(val, connectionName)) : callX(gormEnhancerExpr, 'findTransactionManager', val)
-                Parameter typeParameter = param(CLASS_Type, 'type')
-                Parameter[] params = hasDataSourceProperty ? params(typeParameter, param(STRING_TYPE, 'connectionName')) : params(typeParameter)
-
-                transactionManagerLookupExpr.setMethodTarget(
-                        gormEnhancerExpr.getType().getDeclaredMethod('findTransactionManager', params)
-                )
-            }
-            else {
-                transactionManagerLookupExpr = hasDataSourceProperty ? callX(gormEnhancerExpr, 'findSingleTransactionManager', connectionName) : callX(gormEnhancerExpr, 'findSingleTransactionManager')
-                Parameter[] params = hasDataSourceProperty ? params(param(STRING_TYPE, 'connectionName')) : ZERO_PARAMETERS
-                transactionManagerLookupExpr.setMethodTarget(
-                        gormEnhancerExpr.getType().getDeclaredMethod('findSingleTransactionManager', params)
-                )
+        ClassNode transactionManagerClassNode = make(PlatformTransactionManager)
+        Expression registryExpr = callX(classX(GormRegistry), 'getInstance')
+
+        Expression tmFieldExpr
+        FieldNode userField = declaringClassNode.getField(PROPERTY_TRANSACTION_MANAGER)
+        if (userField != null) {
+            tmFieldExpr = new AttributeExpression(varX('this'), constX(PROPERTY_TRANSACTION_MANAGER))
+        } else {
+            String transactionManagerFieldName = '$' + PROPERTY_TRANSACTION_MANAGER
+            FieldNode tmField = declaringClassNode.getDeclaredField(transactionManagerFieldName)
+            if (tmField == null) {
+                tmField = declaringClassNode.addField(transactionManagerFieldName, Modifier.PRIVATE, transactionManagerClassNode, null)
+                markAsGenerated(declaringClassNode, tmField)
             }
+            tmFieldExpr = varX(tmField)
+        }
+
+        if (declaringClassNode.getMethod(GET_TRANSACTION_MANAGER_METHOD, ZERO_PARAMETERS) == null) {
+            // resolved TM expression for the getter fallback
+            Expression transactionManagerLookupExpr
+            if (implementsInterface(declaringClassNode, 'org.grails.datastore.mapping.services.Service') ||
+                findAnnotation(declaringClassNode, grails.gorm.services.Service) != null) {
 
-            // simply logic for classes that implement Service
-            if (implementsInterface(declaringClassNode, 'org.grails.datastore.mapping.services.Service')) {
-                // Add Method: PlatformTransactionManager getTransactionManager()
-                // if(datastore != null)
-                //     return datastore.transactionManager
-                // else
-                //     return GormEnhancer.findSingleTransactionManager()
-                ClassNode transactionCapableDatastore = make(TransactionCapableDatastore)
-                Expression datastoreVar = castX(transactionCapableDatastore, varX('datastore'))
-                Expression datastoreLookupExpr = datastoreVar
+                // For services, resolve entirely via static bridge
                 if (hasDataSourceProperty) {
-                    datastoreLookupExpr = callD(castX(make(MultipleConnectionSourceCapableDatastore), datastoreVar), 'getDatastoreForConnection', connectionName)
+                    transactionManagerLookupExpr = callX(registryExpr, 'findTransactionManager', args(classX(nonGeneric(declaringClassNode)), connectionName))
+                }
+                else {
+                    transactionManagerLookupExpr = callX(registryExpr, 'findTransactionManager', args(classX(nonGeneric(declaringClassNode))))
                 }
-                Statement ifElse = ifElseS(
-                        notNullX(datastoreVar),
-                        returnS(propX(castX(transactionCapableDatastore, datastoreLookupExpr), PROPERTY_TRANSACTION_MANAGER)),
-                        returnS(transactionManagerLookupExpr)
-                )
-
-                MethodNode methodNode = declaringClassNode.addMethod(GET_TRANSACTION_MANAGER_METHOD,
-                        Modifier.PUBLIC,
-                        transactionManagerClassNode,
-                        ZERO_PARAMETERS, null,
-                        ifElse)
-                markAsGenerated(declaringClassNode, methodNode)
             }
             else {
-                /// Add field: PlatformTransactionManager $transactionManager
-                String transactionManagerFieldName = '$' + PROPERTY_TRANSACTION_MANAGER
-                FieldNode transactionManagerField = declaringClassNode.addField(transactionManagerFieldName, Modifier.PROTECTED, transactionManagerClassNode, null)
-
-                VariableExpression transactionManagerPropertyExpr = varX(transactionManagerField)
-                BlockStatement getterBody = block()
-
-                // this is a hacky workaround that ensures the transaction manager is also set on the spock shared instance which seems to differ for
-                // some reason
-                if (isSubclassOf(declaringClassNode, 'spock.lang.Specification')) {
-                    getterBody.addStatement(
-                            stmt(
-                                    callX(propX(propX(varThis(), 'specificationContext'), 'sharedInstance'),
-                                            SET_TRANSACTION_MANAGER,
-                                            transactionManagerPropertyExpr)
-                            )
-                    )
+                // For regular objects, use the shared resolver
+                if (hasDataSourceProperty) {
+                    transactionManagerLookupExpr = callX(registryExpr, 'findSingleTransactionManager', connectionName)
                 }
+                else {
+                    transactionManagerLookupExpr = callX(registryExpr, 'findSingleTransactionManager')
+                }
+            }
 
-                // Prepare the getTransactionManager() method body
-                // if($transactionManager != null)
-                //     return $transactionManager
-                // else
-                //     return GormEnhancer.findSingleTransactionManager()
-                Statement ifElse = ifElseS(
-                        notNullX(transactionManagerPropertyExpr),
-                        returnS(transactionManagerPropertyExpr),
-                        returnS(transactionManagerLookupExpr)
+            BlockStatement getterBody = new BlockStatement()
+            ClassNode currentTenantHolderClassNode = make(grails.gorm.multitenancy.CurrentTenantHolder)
+            ClassNode connectionSourceClassNode = make(org.grails.datastore.mapping.core.connections.ConnectionSource)
+            VariableExpression tenantIdVar = varX('tenantId', make(Serializable))
+            getterBody.addStatement(
+                declS(tenantIdVar, callX(classX(currentTenantHolderClassNode), 'get'))
+            )
+            
+            BlockStatement ifTenantActiveBody = new BlockStatement()
+            VariableExpression tmVar = varX('tm', transactionManagerClassNode)
+            ifTenantActiveBody.addStatement(
+                declS(tmVar, callX(registryExpr, 'findSingleTransactionManager', callX(tenantIdVar, 'toString')))
+            )
+            ifTenantActiveBody.addStatement(
+                ifS(notNullX(tmVar), returnS(tmVar))
+            )
+            
+            getterBody.addStatement(
+                ifS(
+                    notNullX(tenantIdVar),
+                    ifElseS(
+                        callX(callX(tenantIdVar, 'toString'), 'equals', propX(classX(connectionSourceClassNode), 'DEFAULT')),
+                        new org.codehaus.groovy.ast.stmt.EmptyStatement(),
+                        ifTenantActiveBody
+                    )
                 )
+            )
+            
+            getterBody.addStatement(
+                ifElseS(notNullX(tmFieldExpr), returnS(tmFieldExpr), returnS(transactionManagerLookupExpr))
+            )
 
-                getterBody.addStatement(ifElse)
-
-                // Add Method: PlatformTransactionManager getTransactionManager()
-                MethodNode getterNode = declaringClassNode.addMethod(GET_TRANSACTION_MANAGER_METHOD,
-                        Modifier.PUBLIC,
-                        transactionManagerClassNode,
-                        ZERO_PARAMETERS, null,
-                        getterBody)
-                markAsGenerated(declaringClassNode, getterNode)
-
-                // Prepare setter parameters
-                Parameter p = param(transactionManagerClassNode, PROPERTY_TRANSACTION_MANAGER)
-                Parameter[] parameters = params(p)
-                if (declaringClassNode.getMethod(SET_TRANSACTION_MANAGER, parameters) == null) {
-                    Statement setterBody = assignS(transactionManagerPropertyExpr, varX(p))
-
-                    // Add Setter Method: void setTransactionManager(PlatformTransactionManager transactionManager)
-                    MethodNode setterNode = declaringClassNode.addMethod(SET_TRANSACTION_MANAGER,
-                            Modifier.PUBLIC,
-                            VOID_TYPE,
-                            parameters,
-                            null,
-                            setterBody)
-                    markAsGenerated(declaringClassNode, setterNode)
-                }
-            }
+            MethodNode getterNode = declaringClassNode.addMethod(GET_TRANSACTION_MANAGER_METHOD,
+                    Modifier.PUBLIC,
+                    transactionManagerClassNode,
+                    ZERO_PARAMETERS, null,
+                    getterBody)
+            markAsGenerated(declaringClassNode, getterNode)
+        }
 
+        // Add setter: public void setTransactionManager(PlatformTransactionManager tm)
+        Parameter p = param(transactionManagerClassNode, PROPERTY_TRANSACTION_MANAGER)
+        if (declaringClassNode.getMethod(SET_TRANSACTION_MANAGER, params(p)) == null) {
+            MethodNode setterNode = declaringClassNode.addMethod(SET_TRANSACTION_MANAGER,
+                    Modifier.PUBLIC,
+                    VOID_TYPE,
+                    params(p),
+                    null,
+                    assignS(tmFieldExpr, varX(p)))
+            markAsGenerated(declaringClassNode, setterNode)
         }
     }
 
@@ -358,38 +351,36 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
         // apply @Transaction attributes to properties of $transactionAttribute
         applyTransactionalAttributeSettings(annotationNode, transactionAttributeVar, newMethodBody, classNode, methodNode)
 
-        boolean isMultiTenant = TenantTransform.hasTenantAnnotation(methodNode)
-
+        boolean isMultiTenant = TenantTransform.hasTenantAnnotation(classNode) || TenantTransform.hasTenantAnnotation(methodNode)
         Expression connectionName = annotationNode.getMember('connection')
         if (connectionName == null) {
             connectionName = annotationNode.getMember('value')
         }
-        if (connectionName == null) {
-            if (isMultiTenant) {
-                connectionName = varX('tenantId')
-            }
-        }
         final boolean hasDataSourceProperty = connectionName != null
 
-        // $transactionManager = connection != null ? getTargetDatastore(connection).getTransactionManager() : getTransactionManager()
+        // resolved TM expression
         Expression transactionManagerExpression
-        if (isMultiTenant && hasDataSourceProperty) {
-            Expression targetDatastoreExpr = castX(make(MultiTenantCapableDatastore), callThisD(classNode, 'getTargetDatastore', ZERO_ARGUMENTS))
-            targetDatastoreExpr = castX(make(TransactionCapableDatastore), callX(targetDatastoreExpr, 'getDatastoreForTenantId', connectionName))
-            transactionManagerExpression = castX(make(PlatformTransactionManager), propX(targetDatastoreExpr, PROPERTY_TRANSACTION_MANAGER))
-
+        if (connectionName == null) {
+            // Use the class-level transaction manager (which supports overrides)
+            transactionManagerExpression = callX(varThis(), GET_TRANSACTION_MANAGER_METHOD)
         }
         else if (hasDataSourceProperty) {
-            // callX(varX("this"), "getTargetDatastore", connectionName)
-            def targetDatastoreExpr = castX(make(TransactionCapableDatastore), callThisD(classNode, 'getTargetDatastore', connectionName))
-            transactionManagerExpression = castX(make(PlatformTransactionManager), propX(targetDatastoreExpr, PROPERTY_TRANSACTION_MANAGER))
+            Expression registryExpr = new MethodCallExpression(classX(make(org.grails.datastore.gorm.GormRegistry)), 'getInstance', new org.codehaus.groovy.ast.expr.ArgumentListExpression())
+            transactionManagerExpression = castX(make(PlatformTransactionManager), callX(registryExpr, 'findSingleTransactionManager', connectionName))
         }
         else {
-            transactionManagerExpression = propX(varX('this'), PROPERTY_TRANSACTION_MANAGER)
+            transactionManagerExpression = callX(varThis(), GET_TRANSACTION_MANAGER_METHOD)
         }
 
+        // PlatformTransactionManager $transactionManager = ... resolved TM ...
+        final ClassNode transactionManagerClassNode = make(PlatformTransactionManager)
+        final VariableExpression transactionManagerVar = varX('$transactionManager', transactionManagerClassNode)
+        newMethodBody.addStatement(
+            declS(transactionManagerVar, transactionManagerExpression)
+        )
+
         // GrailsTransactionTemplate $transactionTemplate
-        //           = new GrailsTransactionTemplate(getTransactionManager(), $transactionAttribute )
+        //           = new GrailsTransactionTemplate($transactionManager, $transactionAttribute )
         final ClassNode transactionTemplateClassNode = make(GrailsTransactionTemplate)
         final VariableExpression transactionTemplateVar = varX('$transactionTemplate', transactionTemplateClassNode)
 
@@ -397,7 +388,7 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
             declS(
                 transactionTemplateVar,
                 ctorX(transactionTemplateClassNode, args(
-                    transactionManagerExpression,
+                    transactionManagerVar,
                     transactionAttributeVar
                 ))
             )
@@ -417,6 +408,12 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
     }
 
     protected applyTransactionalAttributeSettings(AnnotationNode annotationNode, VariableExpression transactionAttributeVar, BlockStatement methodBody, ClassNode classNode, MethodNode methodNode) {
+        // Set the transaction name
+        String transactionName = "${classNode.name}.${methodNode.name}"
+        methodBody.addStatement(
+                assignS(propX(transactionAttributeVar, 'name'), new ConstantExpression(transactionName))
+        )
+
         final ClassNode rollbackRuleAttributeClassNode = make(RollbackRuleAttribute)
         final ClassNode noRollbackRuleAttributeClassNode = make(NoRollbackRuleAttribute)
         final Map<String, Expression> members = annotationNode.getMembers()
@@ -427,73 +424,62 @@ class TransactionalTransform extends AbstractDatastoreMethodDecoratingTransforma
         }
 
         members.each { String name, Expression expr ->
-            if (name == 'rollbackFor' || name == 'rollbackForClassName' || name == 'noRollbackFor' || name == 'noRollbackForClassName') {
-                final targetClassNode = (name == 'rollbackFor' || name == 'rollbackForClassName') ? rollbackRuleAttributeClassNode : noRollbackRuleAttributeClassNode
-                name = 'rollbackRules'
-                if (expr instanceof ListExpression) {
-                    for (exprItem in ((ListExpression) expr).expressions) {
-                        appendRuleElement(methodBody, transactionAttributeVar, name, ctorX(targetClassNode, exprItem))
-                    }
-                } else {
-                    appendRuleElement(methodBody, transactionAttributeVar, name, ctorX(targetClassNode, expr))
+            if (name == 'propagation') {
+                Expression valExpr = expr
+                if (expr instanceof PropertyExpression) {
+                    valExpr = callX(expr, 'value')
                 }
-            } else {
-                if (name == 'isolation') {
-                    name = 'isolationLevel'
-                    expr = callX(expr, 'value', ZERO_ARGUMENTS)
-                } else if (name == 'propagation') {
-                    name = 'propagationBehavior'
-                    expr = callX(expr, 'value', ZERO_ARGUMENTS)
+                methodBody.addStatement(
+                    assignS(propX(transactionAttributeVar, 'propagationBehavior'), valExpr)
+                )
+            } else if (name == 'isolation') {
+                Expression valExpr = expr
+                if (expr instanceof PropertyExpression) {
+                    valExpr = callX(expr, 'value')
                 }
+                methodBody.addStatement(
+                    assignS(propX(transactionAttributeVar, 'isolationLevel'), valExpr)
+                )
+            } else if (name == 'timeout') {
+                methodBody.addStatement(
+                    assignS(propX(transactionAttributeVar, name), expr)
+                )
+            } else if (name == 'readOnly') {
+                methodBody.addStatement(
+                    assignS(propX(transactionAttributeVar, name), expr)
+                )
+            } else if (name == 'rollbackFor' || name == 'rollbackForClassName' || name == 'noRollbackFor' || name == 'noRollbackForClassName') {
+                boolean isRollback = name.startsWith('rollbackFor')
+                ClassNode ruleNode = isRollback ? rollbackRuleAttributeClassNode : noRollbackRuleAttributeClassNode
+                String attributeName = 'rollbackRules'
 
-                if (name != 'value') {
+                if (expr instanceof ListExpression) {
+                    for (Expression e in ((ListExpression) expr).getExpressions()) {
+                        methodBody.addStatement(
+                            stmt(callX(propX(transactionAttributeVar, attributeName), 'add', ctorX(ruleNode, args(e))))
+                        )
+                    }
+                } else {
                     methodBody.addStatement(
-                        assignS(propX(transactionAttributeVar, name), expr)
+                        stmt(callX(propX(transactionAttributeVar, attributeName), 'add', ctorX(ruleNode, args(expr))))
                     )
                 }
+            } else if (name != 'connection' && name != 'value') {
+                methodBody.addStatement(
+                        assignS(propX(transactionAttributeVar, name), expr)
+                )
             }
         }
-
-        final transactionName = classNode.name + '.' + methodNode.name
-        methodBody.addStatement(
-            assignS(propX(transactionAttributeVar, 'name'), new ConstantExpression(transactionName))
-        )
-    }
-
-    private void appendRuleElement(BlockStatement methodBody, VariableExpression transactionAttributeVar, String name, Expression expr) {
-        final rollbackRuleAttributeClassNode = make(RollbackRuleAttribute)
-        ClassNode rollbackRulesListClassNode = nonGeneric(make(List), rollbackRuleAttributeClassNode)
-        def getRollbackRules = castX(rollbackRulesListClassNode, buildGetPropertyExpression(transactionAttributeVar, name, transactionAttributeVar.getType()))
-        methodBody.addStatement(
-            stmt(
-                callX(getRollbackRules, 'add', expr)
-            )
-        )
     }
 
     @Override
-    protected boolean hasExcludedAnnotation(MethodNode md) {
-        return super.hasExcludedAnnotation(md) || hasExcludedAnnotation(md, ANNOTATION_NAME_EXCLUDES)
-    }
-
-    /**
-     * Whether the given method has a transactional annotation
-     *
-     * @param md The method node
-     * @return
-     */
-    static boolean hasTransactionalAnnotation(AnnotatedNode md) {
-        for (AnnotationNode annotation : md.getAnnotations()) {
-            if (ANNOTATION_NAME_EXCLUDES.any() { String n -> n == annotation.classNode.name }) {
-                return true
-            }
-        }
-        return false
+    protected String getRenamedMethodPrefix() {
+        return RENAMED_METHOD_PREFIX
     }
 
     @Override
-    protected String getRenamedMethodPrefix() {
-        return RENAMED_METHOD_PREFIX
+    protected boolean hasLocalAnnotation(MethodNode amd, AnnotationNode classAnnotation) {
+        return findAnnotation(amd, Transactional) != null || findAnnotation(amd, ReadOnly) != null || findAnnotation(amd, Rollback) != null
     }
 
     @Override
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractDatastoreMethodDecoratingTransformation.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractDatastoreMethodDecoratingTransformation.groovy
index 7bfa704ea0..821ebab315 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractDatastoreMethodDecoratingTransformation.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractDatastoreMethodDecoratingTransformation.groovy
@@ -26,20 +26,20 @@ import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.FieldNode
 import org.codehaus.groovy.ast.MethodNode
 import org.codehaus.groovy.ast.Parameter
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
 import org.codehaus.groovy.ast.expr.ClassExpression
 import org.codehaus.groovy.ast.expr.Expression
 import org.codehaus.groovy.ast.expr.MethodCallExpression
 import org.codehaus.groovy.ast.expr.VariableExpression
 import org.codehaus.groovy.ast.stmt.BlockStatement
-import org.codehaus.groovy.ast.stmt.Statement
 import org.codehaus.groovy.control.SourceUnit
 
 import org.springframework.beans.factory.annotation.Autowired
 
-import org.grails.datastore.gorm.GormEnhancer
-import org.grails.datastore.gorm.internal.RuntimeSupport
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.mapping.core.Datastore
 import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.reflect.AstUtils
 
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated
 import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE
@@ -47,20 +47,19 @@ import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
 import static org.codehaus.groovy.ast.ClassHelper.make
 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.constX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS
+import static org.codehaus.groovy.ast.tools.GeneralUtils.indexX
 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.returnS
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX
 import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callD
 import static org.grails.datastore.mapping.reflect.AstUtils.ZERO_PARAMETERS
-import static org.grails.datastore.mapping.reflect.AstUtils.addAnnotationOrGetExisting
-import static org.grails.datastore.mapping.reflect.AstUtils.implementsInterface
 import static org.grails.datastore.mapping.reflect.AstUtils.isSpockTest
 
 /**
@@ -92,7 +91,8 @@ abstract class AbstractDatastoreMethodDecoratingTransformation extends AbstractM
         Expression connectionName = annotationNode.getMember('connection')
         boolean hasDataSourceProperty = connectionName != null
         boolean isSpockTest = isSpockTest(declaringClassNode)
-        ClassExpression gormEnhancerExpr = classX(GormEnhancer)
+        MethodCallExpression registryExpr = new MethodCallExpression(classX(GormRegistry), 'getInstance', new ArgumentListExpression())
+        Expression apiResolverExpr = new MethodCallExpression(registryExpr, 'getApiResolver', new ArgumentListExpression())
 
         Expression datastoreAttribute = annotationNode.getMember('datastore')
         ClassNode defaultType = hasDataSourceProperty ? make(MultipleConnectionSourceCapableDatastore) : make(Datastore)
@@ -102,124 +102,106 @@ abstract class AbstractDatastoreMethodDecoratingTransformation extends AbstractM
         MethodCallExpression datastoreLookupCall
         MethodCallExpression datastoreLookupDefaultCall
         if (hasSpecificDatastore) {
-            datastoreLookupDefaultCall = callD(gormEnhancerExpr, 'findDatastoreByType', classX(datastoreType.getPlainNodeReference()))
+            datastoreLookupDefaultCall = callD(apiResolverExpr, 'findDatastoreByType', classX(datastoreType.getPlainNodeReference()))
         }
         else {
-            datastoreLookupDefaultCall = callD(gormEnhancerExpr, 'findSingleDatastore')
+            datastoreLookupDefaultCall = callD(apiResolverExpr, 'findSingleDatastore')
         }
         datastoreLookupCall = callD(datastoreLookupDefaultCall, METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam))
 
-        if (implementsInterface(declaringClassNode, 'org.grails.datastore.mapping.services.Service')) {
-            // simplify logic for services
-            Parameter[] getTargetDatastoreParams = params(connectionNameParam)
-            VariableExpression datastoreVar = varX('datastore', make(Datastore))
-
-            // Add method:
-            // protected Datastore getTargetDatastore(String connectionName)
-            //    if(datastore != null)
-            //      return datastore.getDatastoreForConnection(connectionName)
-            //    else
-            //      return GormEnhancer.findSingleDatastore().getDatastoreForConnection(connectionName)
-
-            if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, getTargetDatastoreParams) == null) {
-                MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PROTECTED, datastoreType, getTargetDatastoreParams, null,
-                        ifElseS(notNullX(datastoreVar),
-                                returnS(callD(castX(make(MultipleConnectionSourceCapableDatastore), datastoreVar), METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam))),
-                                returnS(datastoreLookupCall)
-                        ))
-                markAsGenerated(declaringClassNode, mn)
+        Parameter[] getTargetDatastoreParams = params(connectionNameParam)
+
+        FieldNode targetDatastoreField = declaringClassNode.getField(FIELD_TARGET_DATASTORE)
+        if (targetDatastoreField == null) {
+            targetDatastoreField = declaringClassNode.addField(FIELD_TARGET_DATASTORE, Modifier.PRIVATE, datastoreType, null)
+            markAsGenerated(declaringClassNode, targetDatastoreField)
+        }
+
+        if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, getTargetDatastoreParams) == null && !AstUtils.hasProperty(declaringClassNode, 'targetDatastore')) {
+            // When $targetDatastore is explicitly set (e.g. by setTargetDatastore), it is the authoritative
+            // parent multi-datasource datastore and must be used for connection routing. Falling back to the
+            // API resolver can return a child datastore that doesn't know about sibling connections.
+            ClassNode multipleConnectionDatastoreClassNode = make('org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore')
+            Expression targetDatastoreExpr = varX(targetDatastoreField)
+            Expression lookupDefaultDatastoreExpr = datastoreLookupDefaultCall
+            org.codehaus.groovy.ast.stmt.Statement datastoreLookupCallWithConnection = ifElseS(
+                instanceofX(lookupDefaultDatastoreExpr, multipleConnectionDatastoreClassNode),
+                returnS(callD(castX(multipleConnectionDatastoreClassNode, lookupDefaultDatastoreExpr), METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam))),
+                returnS(callD(lookupDefaultDatastoreExpr, METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam)))
+            )
+            MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PUBLIC, datastoreType, getTargetDatastoreParams, null,
+                    ifElseS(
+                        notNullX(targetDatastoreExpr),
+                        ifElseS(
+                            instanceofX(targetDatastoreExpr, multipleConnectionDatastoreClassNode),
+                            returnS(callD(castX(multipleConnectionDatastoreClassNode, targetDatastoreExpr), METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam))),
+                            returnS(callD(targetDatastoreExpr, METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam)))
+                        ),
+                        datastoreLookupCallWithConnection
+                    )
+            )
+            markAsGenerated(declaringClassNode, mn)
+            if (!isSpockTest) {
                 compileMethodStatically(source, mn)
             }
-            if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, ZERO_PARAMETERS) == null) {
-                MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PROTECTED,  datastoreType, ZERO_PARAMETERS, null,
-                        ifElseS(notNullX(datastoreVar),
-                                returnS(datastoreVar),
-                                returnS(datastoreLookupDefaultCall))
-                )
-                markAsGenerated(declaringClassNode, mn)
+        }
+        if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, ZERO_PARAMETERS) == null && !AstUtils.hasProperty(declaringClassNode, 'targetDatastore')) {
+            MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PUBLIC,  datastoreType, ZERO_PARAMETERS, null,
+                    ifElseS(notNullX(varX(targetDatastoreField)), returnS(varX(targetDatastoreField)), returnS(datastoreLookupDefaultCall))
+            )
+            markAsGenerated(declaringClassNode, mn)
+
+            if (!isSpockTest) {
                 compileMethodStatically(source, mn)
             }
         }
-        else {
-            FieldNode datastoreField = declaringClassNode.getField(FIELD_TARGET_DATASTORE)
-            if (datastoreField == null) {
-                datastoreField = declaringClassNode.addField(FIELD_TARGET_DATASTORE, Modifier.PROTECTED, datastoreType, null)
-
-                Parameter datastoresParam = param(datastoreType.makeArray(), 'datastores')
-                VariableExpression datastoresVar = varX(datastoresParam)
-                Expression datastoreVar = callD(classX(RuntimeSupport), 'findDefaultDatastore', datastoresVar)
-
-                BlockStatement setTargetDatastoreBody
-                VariableExpression datastoreFieldVar = varX(datastoreField)
-
-                Statement assignTargetDatastore = assignS(datastoreFieldVar, datastoreVar)
-                if (hasDataSourceProperty) {
-                    // $targetDatastore = RuntimeSupport.findDefaultDatastore(datastores)
-                    // datastore = datastore.getDatastoreForConnection(connectionName)
-                    setTargetDatastoreBody = block(
-                            assignTargetDatastore,
-                            assignS(datastoreFieldVar, callX(datastoreFieldVar, METHOD_GET_DATASTORE_FOR_CONNECTION, connectionName))
-                    )
-                }
-                else {
-                    setTargetDatastoreBody = block(
-                            assignTargetDatastore
-                    )
-                }
-
-                weaveSetTargetDatastoreBody(source, annotationNode, declaringClassNode, datastoreVar, setTargetDatastoreBody)
-
-                // Add method: @Autowired void setTargetDatastore(Datastore[] datastores)
-                Parameter[] setTargetDatastoreParams = params(datastoresParam)
-                if (declaringClassNode.getMethod('setTargetDatastore', setTargetDatastoreParams) == null) {
-                    MethodNode setTargetDatastoreMethod = declaringClassNode.addMethod('setTargetDatastore', Modifier.PUBLIC, VOID_TYPE, setTargetDatastoreParams, null, setTargetDatastoreBody)
-                    markAsGenerated(declaringClassNode, setTargetDatastoreMethod)
-
-                    // Autowire setTargetDatastore via Spring
-                    addAnnotationOrGetExisting(setTargetDatastoreMethod, Autowired)
-                            .setMember('required', constX(false))
-
-                    compileMethodStatically(source, setTargetDatastoreMethod)
-                }
-
-                // Add method:
-                // protected Datastore getTargetDatastore(String connectionName)
-                //    if($targetDatastore != null)
-                //      return $targetDatastore.getDatastoreForConnection(connectionName)
-                //    else
-                //      return GormEnhancer.findSingleDatastore().getDatastoreForConnection(connectionName)
-
-                Parameter[] getTargetDatastoreParams = params(connectionNameParam)
-
-                if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, getTargetDatastoreParams) == null) {
-                    MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PROTECTED, datastoreType, getTargetDatastoreParams, null,
-                            ifElseS(notNullX(datastoreFieldVar),
-                                    returnS(callX(datastoreFieldVar, METHOD_GET_DATASTORE_FOR_CONNECTION, varX(connectionNameParam))),
-                                    returnS(datastoreLookupCall)
-                            ))
-                    markAsGenerated(declaringClassNode, mn)
-                    if (!isSpockTest) {
-                        compileMethodStatically(source, mn)
-                    }
-                }
-                if (declaringClassNode.getMethod(METHOD_GET_TARGET_DATASTORE, ZERO_PARAMETERS) == null) {
-                    MethodNode mn = declaringClassNode.addMethod(METHOD_GET_TARGET_DATASTORE, Modifier.PROTECTED,  datastoreType, ZERO_PARAMETERS, null,
-                            ifElseS(notNullX(datastoreFieldVar),
-                                    returnS(datastoreFieldVar),
-                                    returnS(datastoreLookupDefaultCall))
-                    )
-                    markAsGenerated(declaringClassNode, mn)
 
-                    if (!isSpockTest) {
-                        compileMethodStatically(source, mn)
-                    }
-                }
+        // Add setter for single datastore
+        Parameter datastoreParam = param(datastoreType, 'd')
+        if (declaringClassNode.getMethod('setTargetDatastore', params(datastoreParam)) == null) {
+            BlockStatement setTargetDatastoreBody = block()
+            setTargetDatastoreBody.addStatement(
+                    assignS(varX(targetDatastoreField), varX(datastoreParam))
+            )
+            weaveSetTargetDatastoreBody(source, annotationNode, declaringClassNode, varX(datastoreParam), setTargetDatastoreBody)
+            MethodNode setterNode = declaringClassNode.addMethod('setTargetDatastore', Modifier.PUBLIC, VOID_TYPE, params(datastoreParam), null, setTargetDatastoreBody)
+            markAsGenerated(declaringClassNode, setterNode)
+        }
+
+        // Add dummy setter for compatibility
+        Parameter datastoresParam = param(datastoreType.makeArray(), 'datastores')
+        if (declaringClassNode.getMethod('setTargetDatastore', params(datastoresParam)) == null) {
+            BlockStatement setTargetDatastoresBody = block()
+            VariableExpression firstDatastore = varX('first')
+            setTargetDatastoresBody.addStatement(
+                    declS(firstDatastore, indexX(varX(datastoresParam), constX(0)))
+            )
+            setTargetDatastoresBody.addStatement(
+                    assignS(varX(targetDatastoreField), firstDatastore)
+            )
+            weaveSetTargetDatastoreBody(source, annotationNode, declaringClassNode, firstDatastore, setTargetDatastoresBody)
+
+            MethodNode setterNode = declaringClassNode.addMethod('setTargetDatastore', Modifier.PUBLIC, VOID_TYPE, params(datastoresParam), null, setTargetDatastoresBody)
+            markAsGenerated(declaringClassNode, setterNode)
+            if (!AstUtils.hasAnnotation(setterNode, Autowired)) {
+                AnnotationNode autowired = new AnnotationNode(make(Autowired))
+                autowired.addMember('required', constX(false))
+                setterNode.addAnnotation(autowired)
             }
         }
-    }
+        return
+        }
 
     protected void weaveSetTargetDatastoreBody(SourceUnit source, AnnotationNode annotationNode, ClassNode declaringClassNode, Expression datastoreVar, BlockStatement setTargetDatastoreBody) {
         // no-op
     }
 
+    private static Expression instanceofX(Expression objectExpression, ClassNode type) {
+        return org.codehaus.groovy.ast.tools.GeneralUtils.binX(
+            objectExpression,
+            org.codehaus.groovy.ast.tools.GeneralUtils.INSTANCEOF,
+            classX(type)
+        )
+    }
+
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractMethodDecoratingTransformation.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractMethodDecoratingTransformation.groovy
index 4ec411ce4f..557c9f1bf1 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractMethodDecoratingTransformation.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/transform/AbstractMethodDecoratingTransformation.groovy
@@ -81,7 +81,7 @@ import static org.grails.datastore.mapping.reflect.AstUtils.processVariableScope
 @CompileStatic
 abstract class AbstractMethodDecoratingTransformation extends AbstractGormASTTransformation {
 
-    private static final Set<String> METHOD_NAME_EXCLUDES = new HashSet<String>(Arrays.asList('afterPropertiesSet', 'destroy'))
+    private static final Set<String> METHOD_NAME_EXCLUDES = new HashSet<String>(Arrays.asList('afterPropertiesSet', 'destroy', 'getTargetDatastore', 'setTargetDatastore', 'getTransactionManager', 'setTransactionManager'))
     private static final Set<String> ANNOTATION_NAME_EXCLUDES = new HashSet<String>(Arrays.asList(PostConstruct.getName(), PreDestroy.getName(), 'grails.web.controllers.ControllerMethod'))
     /**
      * Key used to store within the original method node metadata, all previous decorated methods
@@ -124,6 +124,7 @@ abstract class AbstractMethodDecoratingTransformation extends AbstractGormASTTra
             if (!md.isSynthetic() && Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers) &&
                     !Modifier.isStatic(modifiers) && !hasJunitAnnotation(md)) {
                 if (hasExcludedAnnotation(md)) continue
+                if (hasLocalAnnotation(md, annotationNode)) continue
 
                 def startsWithSpock = methodName.startsWith('$spock')
                 if (methodName.contains('$') && !startsWithSpock) continue
@@ -394,4 +395,8 @@ abstract class AbstractMethodDecoratingTransformation extends AbstractGormASTTra
         return excludedAnnotation
     }
 
+    protected boolean hasLocalAnnotation(MethodNode amd, AnnotationNode classAnnotation) {
+        return hasAnnotation(amd, classAnnotation.classNode)
+    }
+
 }
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy
index 7dced16fc7..d58a063c17 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy
@@ -25,7 +25,7 @@ import org.springframework.context.MessageSource
 import org.springframework.validation.Errors
 
 import grails.gorm.DetachedCriteria
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.validation.constraints.AbstractConstraint
 import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
 import org.grails.datastore.mapping.model.MappingContext
@@ -125,7 +125,7 @@ class UniqueConstraint extends AbstractConstraint {
                     return
                 }
                 // replace with proxy to prevent trying to flush transient instance
-                propertyValue = GormEnhancer.findStaticApi(association.javaClass).load(associationId)
+                propertyValue = GormRegistry.instance.findStaticApi(association.javaClass).load(associationId)
             }
         }
 
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
index fce376c31a..9fc28fe118 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
@@ -26,9 +26,11 @@ import jakarta.validation.ConstraintViolation
 import jakarta.validation.Validator
 import jakarta.validation.executable.ExecutableValidator
 
+import org.springframework.validation.Errors
 import org.springframework.validation.beanvalidation.SpringValidatorAdapter
 
 import org.grails.datastore.gorm.GormValidateable
+import org.grails.datastore.gorm.validation.CascadingValidator
 
 /**
  * A validator adapter that applies translates the constraint errors into the Errors object of a GORM entity
@@ -37,7 +39,9 @@ import org.grails.datastore.gorm.GormValidateable
  * @since 6.1
  */
 @CompileStatic
-class GormValidatorAdapter extends SpringValidatorAdapter {
+class GormValidatorAdapter extends SpringValidatorAdapter implements CascadingValidator {
+
+    public static final ThreadLocal<Boolean> CASCADE_VALIDATION = new ThreadLocal<>()
 
     final Validator thisValidator
 
@@ -46,6 +50,17 @@ class GormValidatorAdapter extends SpringValidatorAdapter {
         thisValidator = targetValidator
     }
 
+    @Override
+    void validate(Object obj, Errors errors, boolean cascade) {
+        println "GormValidatorAdapter.validate called with cascade=${cascade}"
+        CASCADE_VALIDATION.set(cascade)
+        try {
+            validate(obj, errors)
+        } finally {
+            CASCADE_VALIDATION.remove()
+        }
+    }
+
     @Override
     def <T> Set<ConstraintViolation<T>> validate(T object, Class<?>[] groups) {
         def constraintViolations = super.validate(object, groups)
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MappingContextTraversableResolver.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MappingContextTraversableResolver.groovy
index c70c6218a3..d86eb6e405 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MappingContextTraversableResolver.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MappingContextTraversableResolver.groovy
@@ -56,6 +56,9 @@ class MappingContextTraversableResolver implements TraversableResolver {
 
     @Override
     boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
+        if (GormValidatorAdapter.CASCADE_VALIDATION.get() == Boolean.FALSE) {
+            return false
+        }
         Class type = proxyHandler.getProxiedClass(traversableObject)
         PersistentEntity entity = mappingContext.getPersistentEntity(type.name)
         if (entity != null) {
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
index b9df7ee32e..f095a10100 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
@@ -25,7 +25,7 @@ import jakarta.persistence.FlushModeType
 
 import org.springframework.context.ApplicationEvent
 
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.GormValidateable
 import org.grails.datastore.gorm.GormValidationApi
 import org.grails.datastore.mapping.core.Datastore
@@ -70,7 +70,7 @@ class ValidationEventListener extends AbstractPersistenceEventListener {
                     boolean hasErrors = false
                     if (source instanceof ConnectionSourcesProvider) {
                         def connectionSourceName = ((ConnectionSourcesProvider) source).connectionSources.defaultConnectionSource.name
-                        GormValidationApi validationApi = GormEnhancer.findValidationApi((Class<Object>) entityObject.getClass(), connectionSourceName)
+                        GormValidationApi validationApi = GormRegistry.instance.findValidationApi((Class<Object>) entityObject.getClass(), connectionSourceName)
                         hasErrors = !validationApi.validate((Object) entityObject)
                     }
... 1 lines suppressed ...


(grails-core) 02/07: fix: restore registerConstraints to GormEnhancer dropped in scaling refactor

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch feat/gorm-registry-core-impl
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit fd1898e11ea3681bda81df6800f7a707b21ecddf
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Fri Jun 26 22:42:27 2026 -0500

    fix: restore registerConstraints to GormEnhancer dropped in scaling refactor
    
    HibernateGormEnhancer (H5 and H7) both declare @Override registerConstraints
    as a no-op. The scaling commit's GormEnhancer refactor omitted this protected
    hook method, making the @Override annotation invalid and causing a Java stub
    compilation error: "method does not override or implement a method from a
    supertype".
    
    Restores the original implementation (loads ConstraintRegistrar via reflection
    if present) and calls it from the constructor, consistent with the pre-scaling
    behaviour.
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../groovy/org/grails/datastore/gorm/GormEnhancer.groovy | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
index a8cbd4f690..67f840dc6b 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
@@ -25,6 +25,7 @@ import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
 import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.util.ClassUtils
 
 import grails.gorm.MultiTenant
 import org.grails.datastore.mapping.core.Datastore
@@ -90,6 +91,8 @@ class GormEnhancer implements Closeable {
         String qualifier = ConnectionSourceNameResolver.resolveDefaultConnectionSourceName(datastore)
         registry.initializeDatastore(datastore, qualifier)
 
+        registerConstraints(datastore)
+
         for (entity in datastore.mappingContext.persistentEntities) {
             registerEntity(entity)
         }
@@ -219,6 +222,19 @@ class GormEnhancer implements Closeable {
         }
     }
 
+    @CompileDynamic
+    protected void registerConstraints(Datastore datastore) {
+        try {
+            String className = 'org.grails.datastore.gorm.support.ConstraintRegistrar'
+            ClassLoader classLoader = getClass().getClassLoader()
+            if (ClassUtils.isPresent(className, classLoader)) {
+                classLoader.loadClass(className).newInstance(datastore)
+            }
+        } catch (Throwable e) {
+            log.debug("Unable to register GORM constraints. Not running a Grails environment. This can be safely ignored if you are not running Grails: $e.message", e)
+        }
+    }
+
     @CompileDynamic
     protected void addStaticMethods(PersistentEntity e) {
         def cls = e.javaClass