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

jamesfredley pushed a commit to branch fix/gorm-api-registration-scaling
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit a6ddce0c16f1b15f8621385369ef4adaf2231f22
Author: James Fredley <[email protected]>
AuthorDate: Fri Jun 26 15:29:04 2026 -0400

    Reduce GORM API allocation for tenant qualifiers
    
    Create datastore routing entries for every expanded qualifier, but only
    eagerly allocate GORM API providers for the canonical qualifier set.
    When a deferred qualifier is used, create and cache its static, instance,
    and validation APIs through the owning enhancer so schema and datasource
    routing continue to use the requested qualifier.
    
    Assisted-by: Hephaestus:openai/gpt-5.5
---
 .../org/grails/datastore/gorm/GormEnhancer.groovy  | 108 +++++++++++++++--
 .../gorm/GormEnhancerAllQualifiersSpec.groovy      | 131 ++++++++++++++++++++-
 2 files changed, 226 insertions(+), 13 deletions(-)

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 3b1d2348e0..7cbe072a13 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
@@ -79,6 +79,8 @@ class GormEnhancer implements Closeable {
         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>()
 
     final Datastore datastore
@@ -122,6 +124,7 @@ class GormEnhancer implements Closeable {
             registerConstraints(datastore)
         }
         NAMED_QUERIES.clear()
+        ENHANCERS.put(datastore, this)
         DATASTORES_BY_TYPE.put(datastore.getClass(), datastore)
 
         for (entity in datastore.mappingContext.persistentEntities) {
@@ -140,10 +143,12 @@ class GormEnhancer implements Closeable {
             def cls = entity.javaClass
 
             List<String> qualifiers = allQualifiers(this.datastore, entity)
-            if (!qualifiers.contains(ConnectionSource.DEFAULT)) {
-                def firstQualifier = qualifiers.first()
+            List<String> apiQualifiers = apiQualifiers(entity, qualifiers)
+            def name = entity.name
+
+            if (!apiQualifiers.contains(ConnectionSource.DEFAULT)) {
+                def firstQualifier = apiQualifiers.first()
                 def staticApi = getStaticApi(cls, firstQualifier)
-                def name = entity.name
                 STATIC_APIS.get(ConnectionSource.DEFAULT).put(name, staticApi)
                 def instanceApi = getInstanceApi(cls, firstQualifier)
                 INSTANCE_APIS.get(ConnectionSource.DEFAULT).put(name, 
instanceApi)
@@ -152,19 +157,45 @@ class GormEnhancer implements Closeable {
                 DATASTORES.get(ConnectionSource.DEFAULT).put(name, 
this.datastore)
 
             }
-            for (qualifier in qualifiers) {
+            for (qualifier in apiQualifiers) {
                 def staticApi = getStaticApi(cls, qualifier)
-                def name = entity.name
                 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)
             }
         }
     }
 
+    /**
+     * 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
      *
@@ -245,12 +276,29 @@ class GormEnhancer implements Closeable {
     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
+        }
+        GormStaticApi<D> staticApi = STATIC_APIS.get(qualifier)?.get(className)
+        if (staticApi != null) {
+            return staticApi
+        }
+        staticApi = enhancer.getStaticApi(entity, qualifier)
+        STATIC_APIS.get(qualifier).put(className, staticApi)
+        return staticApi
+    }
+
     /**
      * Find an instance API for the give entity type and qualifier (the 
connection name)
      *
@@ -261,13 +309,31 @@ class GormEnhancer implements Closeable {
      * @throws IllegalStateException if no instance API is found for the type
      */
     static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity, String 
qualifier = findTenantId(entity)) {
-        def instanceApi = 
INSTANCE_APIS.get(qualifier)?.get(NameUtils.getClassName(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
+        }
+        GormInstanceApi<D> instanceApi = 
INSTANCE_APIS.get(qualifier)?.get(className)
+        if (instanceApi != null) {
+            return instanceApi
+        }
+        instanceApi = enhancer.getInstanceApi(entity, qualifier)
+        INSTANCE_APIS.get(qualifier).put(className, instanceApi)
+        return instanceApi
+    }
+
     /**
      * Find a validation API for the give entity type and qualifier (the 
connection name)
      *
@@ -278,11 +344,34 @@ class GormEnhancer implements Closeable {
      * @throws IllegalStateException if no validation API is found for the type
      */
     static <D> GormValidationApi<D> findValidationApi(Class<D> entity, String 
qualifier = findTenantId(entity)) {
-        def instanceApi = 
VALIDATION_APIS.get(qualifier)?.get(NameUtils.getClassName(entity))
-        if (instanceApi == null) {
+        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 instanceApi
+        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
+        }
+        GormValidationApi<D> validationApi = 
VALIDATION_APIS.get(qualifier)?.get(className)
+        if (validationApi != null) {
+            return validationApi
+        }
+        validationApi = enhancer.getValidationApi(entity, qualifier)
+        VALIDATION_APIS.get(qualifier).put(className, validationApi)
+        return validationApi
+    }
+
+    private static GormEnhancer findEnhancer(Class entity, String qualifier, 
String className) {
+        Datastore datastore = DATASTORES.get(qualifier)?.get(className)
+        datastore != null ? ENHANCERS.get(datastore) : null
     }
 
     /**
@@ -384,6 +473,7 @@ class GormEnhancer implements Closeable {
     @CompileStatic
     void close() throws IOException {
         removeConstraints()
+        ENHANCERS.remove(datastore)
         DATASTORES_BY_TYPE.clear()
         def registry = GroovySystem.metaClassRegistry
         for (entity in datastore.mappingContext.persistentEntities) {
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
index d7da91d7bc..5e87f5109d 100644
--- 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerAllQualifiersSpec.groovy
@@ -24,11 +24,15 @@ import grails.gorm.MultiTenant
 import org.grails.datastore.mapping.config.Entity
 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.ConnectionSources
 import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
 import org.grails.datastore.mapping.model.ClassMapping
 import org.grails.datastore.mapping.model.MappingContext
 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.multitenancy.TenantResolver
 
 /**
  * Tests for {@link GormEnhancer#allQualifiers(Datastore, PersistentEntity)} 
to verify
@@ -41,6 +45,12 @@ import org.grails.datastore.mapping.model.PersistentEntity
  */
 class GormEnhancerAllQualifiersSpec extends Specification {
 
+    private Map<String, PersistentEntity> mockedEntities = [:]
+
+    void cleanup() {
+        mockedEntities.clear()
+    }
+
     /**
      * Create a GormEnhancer with a minimal mock datastore (no entities 
registered).
      */
@@ -64,28 +74,42 @@ class GormEnhancerAllQualifiersSpec extends Specification {
         def classMapping = Mock(ClassMapping) {
             getMappedForm() >> mappedForm
         }
-        Mock(PersistentEntity) {
+        def persistentEntity = Mock(PersistentEntity) {
             getJavaClass() >> javaClass
             getMapping() >> classMapping
             getName() >> javaClass.name
         }
+        mockedEntities[javaClass.name] = persistentEntity
+        persistentEntity
     }
 
     /**
      * Create a mock datastore that also implements ConnectionSourcesProvider,
      * returning the specified connection source names.
      */
-    private Datastore mockMultiConnectionDatastore(List<String> 
connectionNames) {
+    private Datastore mockMultiConnectionDatastore(List<String> 
connectionNames, MultiTenancySettings.MultiTenancyMode mode = 
MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) {
+        def connectionSourceSettings = new ConnectionSourceSettings()
+        connectionSourceSettings.multiTenancy.mode = mode
         def connectionSourceMocks = connectionNames.collect { name ->
             Mock(ConnectionSource) {
                 getName() >> name
+                getSettings() >> connectionSourceSettings
             }
         }
+        def defaultConnectionSource = connectionSourceMocks.find { it.name == 
ConnectionSource.DEFAULT } ?: connectionSourceMocks.first()
+        def mappingContext = Mock(MappingContext) {
+            getPersistentEntities() >> { mockedEntities.values() }
+            getPersistentEntity(_) >> { String name -> mockedEntities[name] }
+        }
         def allSources = Mock(ConnectionSources) {
             getAllConnectionSources() >> connectionSourceMocks
+            getDefaultConnectionSource() >> defaultConnectionSource
         }
-        Mock(TestConnectionSourcesProviderDatastore) {
+        Mock(TestMultiTenantConnectionSourcesProviderDatastore) {
             getConnectionSources() >> allSources
+            getMappingContext() >> mappingContext
+            getMultiTenancyMode() >> mode
+            getTenantResolver() >> Mock(TenantResolver)
         }
     }
 
@@ -140,6 +164,74 @@ class GormEnhancerAllQualifiersSpec extends Specification {
         qualifiers.size() == 3
     }
 
+    void "registerEntity creates expanded MultiTenant qualifier APIs lazily"() 
{
+        given: "a MultiTenant entity that expands across many qualifiers"
+        def datastore = 
mockMultiConnectionDatastore([ConnectionSource.DEFAULT, 'tenantA', 'tenantB'])
+        def enhancer = new CountingGormEnhancer(datastore)
+        def entity = mockEntity(MultiTenantExpandedEntity, 
[ConnectionSource.DEFAULT])
+
+        when: "registering the entity"
+        enhancer.registerEntity(entity)
+
+        then: "only the default APIs are created eagerly"
+        enhancer.staticApiCount == 1
+        enhancer.instanceApiCount == 1
+        enhancer.validationApiCount == 1
+        [email protected]('tenantA').containsKey(entity.name)
+        !GormEnhancer.@STATIC_APIS.get('tenantA').containsKey(entity.name)
+        !GormEnhancer.@INSTANCE_APIS.get('tenantA').containsKey(entity.name)
+        !GormEnhancer.@VALIDATION_APIS.get('tenantA').containsKey(entity.name)
+
+        when: "the qualifier-specific APIs are requested"
+        def staticApi = GormEnhancer.findStaticApi(MultiTenantExpandedEntity, 
'tenantA')
+        def instanceApi = 
GormEnhancer.findInstanceApi(MultiTenantExpandedEntity, 'tenantA')
+        def validationApi = 
GormEnhancer.findValidationApi(MultiTenantExpandedEntity, 'tenantA')
+
+        then: "registered qualifiers create APIs lazily without collapsing to 
the default datastore"
+        staticApi.is(GormEnhancer.findStaticApi(MultiTenantExpandedEntity, 
'tenantA'))
+        instanceApi.is(GormEnhancer.findInstanceApi(MultiTenantExpandedEntity, 
'tenantA'))
+        
validationApi.is(GormEnhancer.findValidationApi(MultiTenantExpandedEntity, 
'tenantA'))
+        !staticApi.is(GormEnhancer.findStaticApi(MultiTenantExpandedEntity, 
ConnectionSource.DEFAULT))
+        
!instanceApi.is(GormEnhancer.findInstanceApi(MultiTenantExpandedEntity, 
ConnectionSource.DEFAULT))
+        
!validationApi.is(GormEnhancer.findValidationApi(MultiTenantExpandedEntity, 
ConnectionSource.DEFAULT))
+        enhancer.staticApiCount == 2
+        enhancer.instanceApiCount == 2
+        enhancer.validationApiCount == 2
+    }
+
+    void "registerEntity creates tenant APIs lazily for database-per-tenant 
qualifiers"() {
+        given: "a database-per-tenant entity that expands across tenant 
qualifiers"
+        def datastore = 
mockMultiConnectionDatastore([ConnectionSource.DEFAULT, 'tenantA', 'tenantB'], 
MultiTenancySettings.MultiTenancyMode.DATABASE)
+        def enhancer = new CountingGormEnhancer(datastore)
+        def entity = mockEntity(DatabaseMultiTenantExpandedEntity, 
[ConnectionSource.DEFAULT])
+
+        when: "registering the entity"
+        enhancer.registerEntity(entity)
+
+        then: "only the default APIs are created eagerly"
+        enhancer.staticApiCount == 1
+        enhancer.instanceApiCount == 1
+        enhancer.validationApiCount == 1
+        [email protected]('tenantA').containsKey(entity.name)
+        !GormEnhancer.@STATIC_APIS.get('tenantA').containsKey(entity.name)
+        !GormEnhancer.@INSTANCE_APIS.get('tenantA').containsKey(entity.name)
+        !GormEnhancer.@VALIDATION_APIS.get('tenantA').containsKey(entity.name)
+
+        when: "the tenant-specific APIs are requested"
+        def staticApi = 
GormEnhancer.findStaticApi(DatabaseMultiTenantExpandedEntity, 'tenantA')
+        def instanceApi = 
GormEnhancer.findInstanceApi(DatabaseMultiTenantExpandedEntity, 'tenantA')
+        def validationApi = 
GormEnhancer.findValidationApi(DatabaseMultiTenantExpandedEntity, 'tenantA')
+
+        then: "database-per-tenant APIs are created lazily and cached per 
tenant datastore"
+        
staticApi.is(GormEnhancer.findStaticApi(DatabaseMultiTenantExpandedEntity, 
'tenantA'))
+        
instanceApi.is(GormEnhancer.findInstanceApi(DatabaseMultiTenantExpandedEntity, 
'tenantA'))
+        
validationApi.is(GormEnhancer.findValidationApi(DatabaseMultiTenantExpandedEntity,
 'tenantA'))
+        
!staticApi.is(GormEnhancer.findStaticApi(DatabaseMultiTenantExpandedEntity, 
ConnectionSource.DEFAULT))
+        enhancer.staticApiCount == 2
+        enhancer.instanceApiCount == 2
+        enhancer.validationApiCount == 2
+    }
+
     void "MultiTenant entity with ALL datasource expands to all qualifiers"() {
         given: "a MultiTenant entity declared with ConnectionSource.ALL"
         def enhancer = createEnhancer()
@@ -225,12 +317,43 @@ class GormEnhancerAllQualifiersSpec extends Specification 
{
     static class MultiTenantDefaultEntity implements 
MultiTenant<MultiTenantDefaultEntity> {}
     static class MultiTenantAllEntity implements 
MultiTenant<MultiTenantAllEntity> {}
     static class MultiTenantMultiDsEntity implements 
MultiTenant<MultiTenantMultiDsEntity> {}
+    static class MultiTenantExpandedEntity implements 
MultiTenant<MultiTenantExpandedEntity> {}
+    static class DatabaseMultiTenantExpandedEntity implements 
MultiTenant<DatabaseMultiTenantExpandedEntity> {}
     static class NonMultiTenantSecondaryEntity {}
     static class NonMultiTenantDefaultEntity {}
     static class NonMultiTenantAllEntity {}
 
+    static class CountingGormEnhancer extends GormEnhancer {
+
+        int staticApiCount
+        int instanceApiCount
+        int validationApiCount
+
+        CountingGormEnhancer(Datastore datastore) {
+            super(datastore)
+        }
+
+        @Override
+        protected <D> GormStaticApi<D> getStaticApi(Class<D> cls, String 
qualifier) {
+            staticApiCount++
+            super.getStaticApi(cls, qualifier)
+        }
+
+        @Override
+        protected <D> GormInstanceApi<D> getInstanceApi(Class<D> cls, String 
qualifier) {
+            instanceApiCount++
+            super.getInstanceApi(cls, qualifier)
+        }
+
+        @Override
+        protected <D> GormValidationApi<D> getValidationApi(Class<D> cls, 
String qualifier) {
+            validationApiCount++
+            super.getValidationApi(cls, qualifier)
+        }
+    }
+
     /**
      * Combined interface so Spock can mock a Datastore that also provides 
ConnectionSources.
      */
-    static interface TestConnectionSourcesProviderDatastore extends Datastore, 
ConnectionSourcesProvider {}
+    static interface TestMultiTenantConnectionSourcesProviderDatastore extends 
MultiTenantCapableDatastore<Object, ConnectionSourceSettings> {}
 }

Reply via email to