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> {} }
