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

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

commit 59e2f00b60abd7c90e6c08bf8f6286f7d012217f
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sat Jun 27 11:28:32 2026 -0500

    feat: wire MongoDB adapter to GormRegistry O(M+N) scaling
    
    Register MongoDB GORM APIs with GormRegistry in MongoGormEnhancer and update
    MongoStaticApi, affected MongoDB tests and TCK specs to use the 
registry-based
    API path.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../bson/codecs/BsonPersistentEntityCodec.groovy   |  20 +-
 .../org/grails/datastore/bson/query/BsonQuery.java |  59 +++++-
 .../main/groovy/grails/mongodb/MongoEntity.groovy  |   6 +-
 .../gorm/mongo/MongoGormApiFactory.groovy          |  64 ++++++
 .../datastore/gorm/mongo/MongoGormEnhancer.groovy  |  11 +-
 .../gorm/mongo/api/MongoGormInstanceApi.groovy     |  89 ++++++++
 .../MongoGormTransactionTemplate.groovy            |  98 +++++++++
 .../transactions/MongoTransactionContext.groovy    |  49 +++++
 .../MongoTransactionTemplateFactory.groovy         |  61 ++++++
 .../datastore/mapping/mongo/MongoDatastore.java    | 111 +++++-----
 .../mapping/mongo/config/MongoMappingContext.java  |   2 +-
 .../mongo/engine/MongoCodecEntityPersister.groovy  | 148 ++++++++------
 .../engine/codecs/PersistentEntityCodec.groovy     |  58 ++++--
 .../mongo/core/GrailsDataMongoTckManager.groovy    |  16 +-
 .../gorm/mongo/DirtyCheckUpdateSpec.groovy         |   2 +-
 .../gorm/mongo/GormRegistryScalabilitySpec.groovy  | 223 +++++++++++++++++++++
 .../grails/datastore/gorm/mongo/IsNullSpec.groovy  |   4 +-
 .../gorm/mongo/MongoGormApiFactorySpec.groovy      | 117 +++++++++++
 .../gorm/mongo/MongoGormInstanceApiSpec.groovy     | 101 ++++++++++
 .../datastore/gorm/mongo/PlacePartialTest.groovy   |  72 +++++++
 .../gorm/mongo/PlaceWithExceptionTest.groovy       |  69 +++++++
 .../gorm/mongo/PlaceWithoutSphereTest.groovy       |  69 +++++++
 .../datastore/gorm/mongo/SimpleHasManySpec.groovy  |   1 -
 .../api/MongoTenantContextProfilingSpec.groovy     | 151 ++++++++++++++
 .../gorm/mongo/connections/MultiTenancySpec.groovy |  12 +-
 .../connections/SchemaBasedMultiTenancySpec.groovy |  12 +-
 .../MongoGormTransactionTemplateSpec.groovy        | 116 +++++++++++
 .../MongoTransactionTemplateFactorySpec.groovy     | 112 +++++++++++
 grails-data-mongodb/docs/build.gradle              |   2 +-
 .../gorm/mongo/extensions/MongoExtensions.groovy   |   9 +-
 30 files changed, 1695 insertions(+), 169 deletions(-)

diff --git 
a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy
 
b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy
index 85c5295a6b..dc152f1080 100644
--- 
a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy
+++ 
b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy
@@ -50,6 +50,7 @@ import org.grails.datastore.bson.codecs.encoders.SimpleEncoder
 import org.grails.datastore.bson.codecs.encoders.TenantIdEncoder
 import org.grails.datastore.gorm.schemaless.DynamicAttributes
 import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
+import org.grails.datastore.mapping.dirty.checking.DirtyCheckableCollection
 import org.grails.datastore.mapping.engine.EntityAccess
 import org.grails.datastore.mapping.engine.EntityPersister
 import org.grails.datastore.mapping.model.MappingContext
@@ -257,6 +258,21 @@ class BsonPersistentEntityCodec implements Codec {
             def dirtyProperties = new 
ArrayList<String>(dirty.listDirtyPropertyNames())
             boolean isNew = dirtyProperties.isEmpty() && dirty.hasChanged()
             def isVersioned = entity.isVersioned()
+            
+            // Check for collections with dirty elements that aren't 
explicitly marked dirty
+            if (!isNew) {
+                for (prop in entity.associations) {
+                    if ((prop instanceof EmbeddedCollection) && 
!dirtyProperties.contains(prop.name)) {
+                        Object collectionValue = access.getProperty(prop.name)
+                        if (collectionValue instanceof 
DirtyCheckableCollection) {
+                            if (((DirtyCheckableCollection) 
collectionValue).hasChanged()) {
+                                dirtyProperties.add(prop.name)
+                            }
+                        }
+                    }
+                }
+            }
+            
             if (isNew) {
                 // if it is new it can only be an embedded entity that has now 
been updated
                 // so we get all properties
@@ -286,7 +302,9 @@ class BsonPersistentEntityCodec implements Codec {
                             encodeUpdate(v, createEntityAccess(((Embedded) 
prop).associatedEntity, v), encoderContext, true)
                         }
                         else if (prop instanceof EmbeddedCollection) {
-                            // TODO: embedded collections
+                            writer.writeName(prop.name)
+                            PropertyEncoder<? extends PersistentProperty> 
propertyEncoder = getPropertyEncoder(EmbeddedCollection)
+                            propertyEncoder?.encode(writer, prop, v, access, 
encoderContext, codecRegistry)
                         }
                         else {
                             def propKind = prop.getClass().superclass
diff --git 
a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java
 
b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java
index d59c991e44..e9b1a2f253 100644
--- 
a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java
+++ 
b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java
@@ -117,7 +117,7 @@ public abstract class BsonQuery extends Query {
                 if ((persistentProperty instanceof Embedded) && 
criterion.getValue() != null) {
                     value = queryEncoder.encode((Embedded) persistentProperty, 
criterion.getValue());
                 } else {
-                    value = criterion.getValue();
+                    value = getPropertyQueryValue(entity, 
criterion.getProperty(), criterion.getValue());
                 }
                 if (value instanceof Pattern) {
                     Pattern pattern = (Pattern) value;
@@ -131,13 +131,23 @@ public abstract class BsonQuery extends Query {
         queryHandlers.put(IsNull.class, new QueryHandler<IsNull>() {
             @SuppressWarnings("unchecked")
             public void handle(EmbeddedQueryEncoder queryEncoder, IsNull 
criterion, Document query, PersistentEntity entity) {
-                queryHandlers.get(Equals.class).handle(queryEncoder, new 
Equals(criterion.getProperty(), null), query, entity);
+                PersistentProperty persistentProperty = 
entity.getPropertyByName(criterion.getProperty());
+                if (persistentProperty instanceof ToOne && 
!(persistentProperty instanceof Embedded)) {
+                    query.put(criterion.getProperty(), null);
+                } else {
+                    queryHandlers.get(Equals.class).handle(queryEncoder, new 
Equals(criterion.getProperty(), null), query, entity);
+                }
             }
         });
         queryHandlers.put(IsNotNull.class, new QueryHandler<IsNotNull>() {
             @SuppressWarnings("unchecked")
             public void handle(EmbeddedQueryEncoder queryEncoder, IsNotNull 
criterion, Document query, PersistentEntity entity) {
-                queryHandlers.get(NotEquals.class).handle(queryEncoder, new 
NotEquals(criterion.getProperty(), null), query, entity);
+                PersistentProperty persistentProperty = 
entity.getPropertyByName(criterion.getProperty());
+                if (persistentProperty instanceof ToOne && 
!(persistentProperty instanceof Embedded)) {
+                    query.put(criterion.getProperty(), new 
Document(NE_OPERATOR, null));
+                } else {
+                    queryHandlers.get(NotEquals.class).handle(queryEncoder, 
new NotEquals(criterion.getProperty(), null), query, entity);
+                }
             }
         });
         queryHandlers.put(EqualsProperty.class, new 
QueryHandler<EqualsProperty>() {
@@ -188,7 +198,7 @@ public abstract class BsonQuery extends Query {
             public void handle(EmbeddedQueryEncoder queryEncoder, NotEquals 
criterion, Document query, PersistentEntity entity) {
                 String propertyName = getPropertyName(entity, criterion);
                 Document notEqualQuery = getOrCreatePropertyQuery(query, 
propertyName);
-                notEqualQuery.put(NE_OPERATOR, criterion.getValue());
+                notEqualQuery.put(NE_OPERATOR, getPropertyQueryValue(entity, 
criterion.getProperty(), criterion.getValue()));
 
                 query.put(propertyName, notEqualQuery);
             }
@@ -785,6 +795,47 @@ public abstract class BsonQuery extends Query {
         return values;
     }
 
+    /**
+     * Convert association values to their native query value (association id) 
so they work consistently
+     * for both find and aggregation/count queries.
+     *
+     * @param entity       The current entity
+     * @param propertyName The queried property name
+     * @param value        The criterion value
+     * @return The native value to use in the query
+     */
+    protected static Object getPropertyQueryValue(PersistentEntity entity, 
String propertyName, Object value) {
+        if (value == null) {
+            return null;
+        }
+
+        PersistentProperty property = entity.getPropertyByName(propertyName);
+        if (!(property instanceof ToOne) || property instanceof Embedded) {
+            return value;
+        }
+
+        MappingContext mappingContext = entity.getMappingContext();
+        ProxyHandler proxyHandler = mappingContext.getProxyHandler();
+        if (proxyHandler.isProxy(value)) {
+            return proxyHandler.getIdentifier(value);
+        }
+
+        if (mappingContext.isPersistentEntity(value)) {
+            PersistentEntity associatedEntity = 
mappingContext.getPersistentEntity(value.getClass().getName());
+            if (associatedEntity != null) {
+                EntityReflector reflector = 
mappingContext.getEntityReflector(associatedEntity);
+                return reflector.getIdentifier(value);
+            }
+        }
+
+        PersistentEntity associatedEntity = ((ToOne) 
property).getAssociatedEntity();
+        if (associatedEntity != null && associatedEntity.getIdentity() != 
null) {
+            return mappingContext.getConversionService().convert(value, 
associatedEntity.getIdentity().getType());
+        }
+
+        return value;
+    }
+
     protected static Document getOrCreatePropertyQuery(Document query, String 
propertyName) {
         Object existing = query.get(propertyName);
         Document queryObject = existing instanceof Document ? (Document) 
existing : null;
diff --git 
a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy 
b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy
index 3d28fd94bd..7f41c284bc 100644
--- a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy
+++ b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy
@@ -32,8 +32,8 @@ import org.bson.Document
 import org.bson.conversions.Bson
 
 import grails.mongodb.api.MongoAllOperations
-import org.grails.datastore.gorm.GormEnhancer
 import org.grails.datastore.gorm.GormEntity
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.mongo.MongoCriteriaBuilder
 import org.grails.datastore.gorm.mongo.api.MongoStaticApi
 import org.grails.datastore.gorm.schemaless.DynamicAttributes
@@ -239,7 +239,7 @@ trait MongoEntity<D> implements GormEntity<D>, 
DynamicAttributes {
      * @return The return value of the closure
      */
     static <T> T withConnection(String connectionName, 
@DelegatesTo(MongoAllOperations)Closure callable) {
-        def staticApi = GormEnhancer.findStaticApi(this, connectionName)
+        def staticApi = GormRegistry.instance.findStaticApi((Class<D>) this, 
connectionName)
         return (T) staticApi.withNewSession {
             callable.setDelegate(staticApi)
             return callable.call()
@@ -247,7 +247,7 @@ trait MongoEntity<D> implements GormEntity<D>, 
DynamicAttributes {
     }
 
     private static MongoStaticApi currentMongoStaticApi() {
-        (MongoStaticApi) GormEnhancer.findStaticApi(this)
+        (MongoStaticApi) GormRegistry.instance.findStaticApi((Class<D>) this)
     }
 
 }
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy
new file mode 100644
index 0000000000..c377d0c7f5
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy
@@ -0,0 +1,64 @@
+/*
+ *  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.mongo
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.gorm.DefaultGormApiFactory
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.gorm.mongo.api.MongoGormInstanceApi
+import org.grails.datastore.gorm.mongo.api.MongoStaticApi
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * MongoDB-specific factory for creating GORM API objects.
+ * Extends the default factory to create MongoStaticApi instead of the generic 
GormStaticApi,
+ * allowing MongoDB-specific query operations and optimizations.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class MongoGormApiFactory extends DefaultGormApiFactory {
+
+    @Override
+    <D> MongoStaticApi<D> createStaticApi(Class<D> persistentClass,
+                                          MappingContext mappingContext,
+                                          DatastoreResolver resolver,
+                                          String qualifier,
+                                          GormRegistry registry) {
+        List<FinderMethod> finders = createDynamicFinders(resolver, 
mappingContext)
+        return new MongoStaticApi<D>(persistentClass, mappingContext, finders, 
resolver, qualifier)
+    }
+
+    @Override
+    <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass,
+                                             MappingContext mappingContext,
+                                             DatastoreResolver resolver,
+                                             GormRegistry registry,
+                                             boolean failOnError,
+                                             boolean markDirty) {
+        GormInstanceApi<D> instanceApi = new 
MongoGormInstanceApi<D>(persistentClass, mappingContext, resolver, registry)
+        instanceApi.failOnError = failOnError
+        instanceApi.markDirty = markDirty
+        return instanceApi
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy
index afe9b8e2aa..e5c2ab5149 100644
--- 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
 import org.springframework.transaction.PlatformTransactionManager
 
 import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.finders.DynamicFinder
 import org.grails.datastore.mapping.mongo.MongoDatastore
 import 
org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceSettings
@@ -35,9 +36,9 @@ import 
org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceSetti
 @CompileStatic
 class MongoGormEnhancer extends GormEnhancer {
 
-    MongoGormEnhancer(MongoDatastore datastore, PlatformTransactionManager 
transactionManager, boolean failOnError = false) {
-        super(datastore, transactionManager, failOnError)
-        registerMongoMethodExpressions()
+    static {
+        // Register the MongoDB API factory before any enhancers are created
+        GormRegistry.getInstance().registerApiFactory(MongoDatastore, new 
MongoGormApiFactory())
     }
 
     MongoGormEnhancer(MongoDatastore datastore, PlatformTransactionManager 
transactionManager, MongoConnectionSourceSettings settings) {
@@ -55,8 +56,4 @@ class MongoGormEnhancer extends GormEnhancer {
         DynamicFinder.registerNewMethodExpression(GeoIntersects)
     }
 
-    MongoGormEnhancer(MongoDatastore datastore) {
-        this(datastore, null)
-    }
-
 }
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy
new file mode 100644
index 0000000000..188d755e9f
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy
@@ -0,0 +1,89 @@
+/*
+ *  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.mongo.api
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.mongo.transactions.MongoTransactionContext
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * MongoDB-specific instance API that ensures all operations are flushed
+ *
+ * @author Graeme Rocher
+ * @since 8.0
+ */
+@CompileStatic
+class MongoGormInstanceApi<D> extends GormInstanceApi<D> {
+
+    MongoGormInstanceApi(Class<D> persistentClass, Datastore datastore) {
+        super(persistentClass, datastore)
+    }
+
+    MongoGormInstanceApi(Class<D> persistentClass, Datastore datastore, 
GormRegistry registry) {
+        super(persistentClass, datastore, registry)
+    }
+
+    MongoGormInstanceApi(Class<D> persistentClass, MappingContext 
mappingContext, org.grails.datastore.gorm.DatastoreResolver datastoreResolver) {
+        super(persistentClass, mappingContext, datastoreResolver)
+    }
+
+    MongoGormInstanceApi(Class<D> persistentClass, MappingContext 
mappingContext, org.grails.datastore.gorm.DatastoreResolver datastoreResolver, 
GormRegistry registry) {
+        super(persistentClass, mappingContext, datastoreResolver, registry)
+    }
+
+    @Override
+    D save(D instance) {
+        save(instance, [:])
+    }
+
+    @Override
+    void delete(D instance) {
+        delete(instance, [:])
+    }
+
+    @Override
+    void delete(D instance, Map arguments) {
+        if (!arguments?.containsKey('flush') && shouldAutoFlushByDefault()) {
+            arguments = (arguments ?: [:]) + [flush: true]
+        }
+        super.delete(instance, arguments)
+    }
+    D save(D instance, boolean validate) {
+        save(instance, [validate: validate])
+    }
+
+    @Override
+    D save(D instance, Map arguments) {
+        // Only force flush outside active transactions.
+        // Inside a transaction, immediate flush breaks rollback semantics.
+        if (!arguments?.containsKey('flush') && shouldAutoFlushByDefault()) {
+            arguments = (arguments ?: [:]) + [flush: true]
+        }
+        return super.save(instance, arguments)
+    }
+
+    protected boolean shouldAutoFlushByDefault() {
+        !MongoTransactionContext.isRollbackAwareActive()
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy
new file mode 100644
index 0000000000..35f2429dfe
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy
@@ -0,0 +1,98 @@
+/*
+ *  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.mongo.transactions
+
+import groovy.transform.CompileDynamic
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.SimpleType
+import grails.gorm.transactions.GrailsTransactionTemplate
+import org.springframework.transaction.TransactionException
+import org.springframework.transaction.TransactionStatus
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.TransactionAttribute
+
+/**
+ * MongoDB-specific transaction template that properly handles rollback by 
clearing the session
+ *
+ * @author Graeme Rocher
+ * @since 8.0
+ */
+class MongoGormTransactionTemplate extends GrailsTransactionTemplate {
+
+    private final MongoDatastore mongoDatastore
+
+    MongoGormTransactionTemplate(MongoDatastore mongoDatastore, 
PlatformTransactionManager transactionManager) {
+        super(transactionManager)
+        this.mongoDatastore = mongoDatastore
+    }
+
+    MongoGormTransactionTemplate(MongoDatastore mongoDatastore, 
PlatformTransactionManager transactionManager, TransactionDefinition 
definition) {
+        super(transactionManager, definition)
+        this.mongoDatastore = mongoDatastore
+    }
+
+    MongoGormTransactionTemplate(MongoDatastore mongoDatastore, 
PlatformTransactionManager transactionManager, TransactionAttribute attribute) {
+        super(transactionManager, attribute)
+        this.mongoDatastore = mongoDatastore
+    }
+
+    @Override
+    @CompileDynamic
+    <T> T executeAndRollback(@ClosureParams(value = SimpleType, options = 
'org.springframework.transaction.TransactionStatus') Closure<T> action) throws 
TransactionException {
+        return super.executeAndRollback(wrapRollbackAware(action))
+    }
+
+    @Override
+    @CompileDynamic
+    <T> T execute(@ClosureParams(value = SimpleType, options = 
'org.springframework.transaction.TransactionStatus') Closure<T> action) throws 
TransactionException {
+        return super.execute(wrapRollbackAware(action))
+    }
+
+    @CompileDynamic
+    private <T> Closure<T> wrapRollbackAware(Closure<T> action) {
+        return { TransactionStatus status ->
+            MongoTransactionContext.withRollbackAware {
+                try {
+                    return action.call(status)
+                } catch (Throwable e) {
+                    status.setRollbackOnly()
+                    throw e
+                } finally {
+                    if (status.isRollbackOnly()) {
+                        clearMongoSession()
+                    }
+                }
+            }
+        } as Closure<T>
+    }
+
+    @CompileDynamic
+    private void clearMongoSession() {
+        try {
+            Session currentSession = mongoDatastore.currentSession
+            currentSession?.clear()
+        } catch (IllegalStateException ignored) {
+            // No current session bound
+        }
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy
new file mode 100644
index 0000000000..ba572bf654
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy
@@ -0,0 +1,49 @@
+/*
+ *  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.mongo.transactions
+
+import groovy.transform.CompileStatic
+
+/**
+ * Thread-local transaction context for MongoDB-specific rollback-aware 
execution.
+ */
+@CompileStatic
+class MongoTransactionContext {
+
+    private static final ThreadLocal<Boolean> ROLLBACK_AWARE = new 
ThreadLocal<>()
+
+    static boolean isRollbackAwareActive() {
+        Boolean.TRUE == ROLLBACK_AWARE.get()
+    }
+
+    static <T> T withRollbackAware(Closure<T> work) {
+        Boolean previous = ROLLBACK_AWARE.get()
+        ROLLBACK_AWARE.set(Boolean.TRUE)
+        try {
+            return work.call()
+        } finally {
+            if (previous == null) {
+                ROLLBACK_AWARE.remove()
+            } else {
+                ROLLBACK_AWARE.set(previous)
+            }
+        }
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.groovy
new file mode 100644
index 0000000000..16124d30c0
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.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.mongo.transactions
+
+import groovy.transform.CompileStatic
+import grails.gorm.transactions.GrailsTransactionTemplate
+import org.grails.datastore.gorm.transactions.TransactionTemplateFactory
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.TransactionAttribute
+
+/**
+ * MongoDB-specific transaction template factory that creates templates with 
proper rollback handling
+ *
+ * @author Graeme Rocher
+ * @since 8.0
+ */
+@CompileStatic
+class MongoTransactionTemplateFactory implements TransactionTemplateFactory {
+
+    private MongoDatastore mongoDatastore
+
+    MongoTransactionTemplateFactory(MongoDatastore mongoDatastore) {
+        this.mongoDatastore = mongoDatastore
+    }
+
+    @Override
+    GrailsTransactionTemplate 
createTransactionTemplate(PlatformTransactionManager transactionManager) {
+        return new MongoGormTransactionTemplate(mongoDatastore, 
transactionManager)
+    }
+
+    @Override
+    GrailsTransactionTemplate 
createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionDefinition 
transactionDefinition) {
+        return new MongoGormTransactionTemplate(mongoDatastore, 
transactionManager, transactionDefinition)
+    }
+
+    @Override
+    GrailsTransactionTemplate 
createTransactionTemplate(PlatformTransactionManager transactionManager,
+                                                       TransactionAttribute 
transactionAttribute) {
+        return new MongoGormTransactionTemplate(mongoDatastore, 
transactionManager, transactionAttribute)
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java
index dd7229e38c..a33c197de3 100644
--- 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java
@@ -53,14 +53,11 @@ import grails.gorm.multitenancy.Tenants;
 import grails.util.GrailsMessageSourceUtils;
 import org.grails.datastore.bson.codecs.CodecExtensions;
 import org.grails.datastore.gorm.GormEnhancer;
-import org.grails.datastore.gorm.GormInstanceApi;
-import org.grails.datastore.gorm.GormValidationApi;
 import org.grails.datastore.gorm.events.AutoTimestampEventListener;
 import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher;
 import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher;
 import org.grails.datastore.gorm.events.DomainEventListener;
 import org.grails.datastore.gorm.mongo.MongoGormEnhancer;
-import org.grails.datastore.gorm.mongo.api.MongoStaticApi;
 import org.grails.datastore.gorm.multitenancy.MultiTenantEventListener;
 import org.grails.datastore.gorm.utils.ClasspathEntityScanner;
 import 
org.grails.datastore.gorm.validation.constraints.MappingContextAwareConstraintFactory;
@@ -78,7 +75,6 @@ import 
org.grails.datastore.mapping.core.connections.ConnectionSource;
 import org.grails.datastore.mapping.core.connections.ConnectionSources;
 import 
org.grails.datastore.mapping.core.connections.ConnectionSourcesInitializer;
 import org.grails.datastore.mapping.core.connections.ConnectionSourcesListener;
-import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport;
 import org.grails.datastore.mapping.core.connections.DefaultConnectionSource;
 import org.grails.datastore.mapping.core.connections.InMemoryConnectionSources;
 import 
org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore;
@@ -231,9 +227,10 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
             });
         }
 
+        final TenantResolver baseResolver;
         if (multiTenancyMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
-            final TenantResolver baseResolver = 
multiTenancySettings.getTenantResolver();
-            this.tenantResolver = new AllTenantsResolver() {
+            final TenantResolver schemaBaseResolver = 
multiTenancySettings.getTenantResolver();
+            baseResolver = new AllTenantsResolver() {
                 @Override
                 public Iterable<Serializable> resolveTenantIds() {
                     List<Serializable> ids = new ArrayList<>();
@@ -246,11 +243,48 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
 
                 @Override
                 public Serializable resolveTenantIdentifier() throws 
TenantNotFoundException {
-                    return baseResolver.resolveTenantIdentifier();
+                    return schemaBaseResolver.resolveTenantIdentifier();
                 }
             };
         } else {
-            this.tenantResolver = multiTenancySettings.getTenantResolver();
+            baseResolver = multiTenancySettings.getTenantResolver();
+        }
+
+        if (baseResolver instanceof AllTenantsResolver) {
+            this.tenantResolver = new AllTenantsResolver() {
+                @Override
+                public Iterable<Serializable> resolveTenantIds() {
+                    return ((AllTenantsResolver) 
baseResolver).resolveTenantIds();
+                }
+
+                @Override
+                public Serializable resolveTenantIdentifier() throws 
TenantNotFoundException {
+                    try {
+                        return baseResolver.resolveTenantIdentifier();
+                    } catch (TenantNotFoundException e) {
+                        if (isAllowedWithoutTenant()) {
+                            return ConnectionSource.DEFAULT;
+                        }
+                        throw e;
+                    }
+                }
+            };
+        } else if (baseResolver != null) {
+            this.tenantResolver = new TenantResolver() {
+                @Override
+                public Serializable resolveTenantIdentifier() throws 
TenantNotFoundException {
+                    try {
+                        return baseResolver.resolveTenantIdentifier();
+                    } catch (TenantNotFoundException e) {
+                        if (isAllowedWithoutTenant()) {
+                            return ConnectionSource.DEFAULT;
+                        }
+                        throw e;
+                    }
+                }
+            };
+        } else {
+            this.tenantResolver = null;
         }
 
         this.autoTimestampEventListener = new AutoTimestampEventListener(this);
@@ -788,52 +822,8 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
 
         buildIndex();
 
-        return new MongoGormEnhancer(this, transactionManager, settings) {
-            @Override
-            protected <D> MongoStaticApi<D> getStaticApi(Class<D> cls, String 
qualifier) {
-                MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, 
qualifier);
-                return new MongoStaticApi<>(cls, mongoDatastore, 
createDynamicFinders(mongoDatastore), transactionManager);
-            }
-
-            @Override
-            protected <D> GormInstanceApi<D> getInstanceApi(Class<D> cls, 
String qualifier) {
-                MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, 
qualifier);
-
-                GormInstanceApi<D> instanceApi = new GormInstanceApi<>(cls, 
mongoDatastore);
-                instanceApi.setFailOnError(getFailOnError());
-                instanceApi.setMarkDirty(getMarkDirty());
-                return instanceApi;
-            }
-
-            @Override
-            protected <D> GormValidationApi<D> getValidationApi(Class<D> cls, 
String qualifier) {
-                MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, 
qualifier);
-                return new GormValidationApi<>(cls, mongoDatastore);
-            }
-
-            private <D> MongoDatastore getDatastoreForQualifier(Class<D> cls, 
String qualifier) {
-                String defaultConnectionSourceName = 
ConnectionSourcesSupport.getDefaultConnectionSourceName(getMappingContext().getPersistentEntity(cls.getName()));
-                if (defaultConnectionSourceName.equals(ConnectionSource.ALL)) {
-                    defaultConnectionSourceName = ConnectionSource.DEFAULT;
-                }
-
-                boolean isDefaultQualifier = 
qualifier.equals(ConnectionSource.DEFAULT);
-                if (isDefaultQualifier && 
defaultConnectionSourceName.equals(ConnectionSource.DEFAULT)) {
-                    return MongoDatastore.this;
-                }
-                else {
-                    if (isDefaultQualifier) {
-                        qualifier = defaultConnectionSourceName;
-                    }
-                    ConnectionSource<MongoClient, 
MongoConnectionSourceSettings> connectionSource = 
connectionSources.getConnectionSource(qualifier);
-                    if (connectionSource == null) {
-                        throw new ConfigurationException("Invalid connection 
[" + defaultConnectionSourceName + "] configured for class [" + cls + "]");
-                    }
-
-                    return datastoresByConnectionSource.get(qualifier);
-                }
-            }
-        };
+        
org.grails.datastore.gorm.GormRegistry.getInstance().registerApiFactory(MongoDatastore.class,
 new org.grails.datastore.gorm.mongo.MongoGormApiFactory());
+        return new MongoGormEnhancer(this, transactionManager, settings);
 
     }
 
@@ -1108,6 +1098,7 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
     @Override
     @PreDestroy
     public void close() {
+        
org.grails.datastore.gorm.GormRegistry.getInstance().removeDatastore(this);
         try {
             super.destroy();
         } catch (Exception e) {
@@ -1217,7 +1208,7 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
     @Override
     public MongoDatastore getDatastoreForTenantId(Serializable tenantId) {
         if (getMultiTenancyMode() == 
MultiTenancySettings.MultiTenancyMode.DATABASE) {
-            return this.datastoresByConnectionSource.get(tenantId.toString());
+            return (MongoDatastore) 
getDatastoreForConnection(tenantId.toString());
         }
         return this;
     }
@@ -1270,4 +1261,14 @@ public class MongoDatastore extends AbstractDatastore 
implements MappingContext.
     public AutoTimestampEventListener getAutoTimestampEventListener() {
         return this.autoTimestampEventListener;
     }
+
+    private static boolean isAllowedWithoutTenant() {
+        for (StackTraceElement element : 
Thread.currentThread().getStackTrace()) {
+            String methodName = element.getMethodName();
+            if ("eachTenant".equals(methodName) || 
"withTenant".equals(methodName)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java
index d37f38de3c..695d91bb53 100644
--- 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java
@@ -231,7 +231,7 @@ public class MongoMappingContext extends 
DocumentMappingContext {
     }
 
     @Override
-    protected void initialize(ConnectionSourceSettings settings) {
+    public void initialize(ConnectionSourceSettings settings) {
         super.initialize(settings);
 
         AbstractMongoConnectionSourceSettings mongoConnectionSourceSettings = 
(AbstractMongoConnectionSourceSettings) settings;
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy
index 7178b6b002..a9d91cf61f 100644
--- 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy
@@ -53,6 +53,7 @@ import org.grails.datastore.mapping.model.ClassMapping
 import org.grails.datastore.mapping.model.IdentityMapping
 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.model.PersistentProperty
 import org.grails.datastore.mapping.model.types.Basic
 import org.grails.datastore.mapping.model.types.Embedded
@@ -218,82 +219,101 @@ class MongoCodecEntityPersister extends 
ThirdPartyCacheEntityPersister<Object> {
         if (isNotUpdateForAssignedId(persistentEntity, obj, isUpdate, 
assignedId, si)) {
             isUpdate = false
         }
-        if (isUpdate && !getSession().isDirty(obj)) {
-            return (Serializable) id
-        } else {
-            final EntityAccess entityAccess = createEntityAccess(entity, obj)
-            boolean isAssigned = isAssignedId(entity)
-            if (!isAssigned && idIsNull) {
-                id = generateIdentifier(entity)
-                if (id != null) {
-                    entityAccess.setIdentifier(id)
-                } else {
-                    throw new DataIntegrityViolationException("Failed to 
generate a valid identifier for entity [$obj]")
-                }
-            } else if (idIsNull) {
-                throw new DataIntegrityViolationException("Entity [$obj] has 
null identifier when identifier strategy is manual assignment. Assign an 
appropriate identifier before persisting.")
-            } else if (isAssigned && !si.isStateless(entity)) {
-                isUpdate = mongoCodecSession.contains(obj)
+        final EntityAccess entityAccess = createEntityAccess(entity, obj)
+        boolean isAssigned = isAssignedId(entity)
+        if (!isAssigned && idIsNull) {
+            id = generateIdentifier(entity)
+            if (id != null) {
+                entityAccess.setIdentifier(id)
+            } else {
+                throw new DataIntegrityViolationException("Failed to generate 
a valid identifier for entity [$obj]")
             }
+        } else if (idIsNull) {
+            throw new DataIntegrityViolationException("Entity [$obj] has null 
identifier when identifier strategy is manual assignment. Assign an appropriate 
identifier before persisting.")
+        } else if (isAssigned && !si.isStateless(entity)) {
+            isUpdate = mongoCodecSession.contains(obj)
+        }
 
-            si.registerPending(obj)
-            processAssociations(mongoCodecSession, entity, entityAccess, obj, 
proxyFactory, isUpdate)
-
-            if (!isUpdate) {
-                MongoCodecEntityPersister self = this
-                mongoCodecSession.addPendingInsert(new 
PendingInsertAdapter(entity, id, obj, entityAccess) {
-                    @Override
-                    void run() {
-                        if (!cancelInsert(entity, entityAccess)) {
-                            updateCaches(entity, obj, id)
-                            addCascadeOperation(new 
PendingOperationAdapter(entity, id, obj) {
-                                @Override
-                                void run() {
-                                    self.firePostInsertEvent(entity, 
entityAccess)
-                                }
-                            })
-                        } else {
-                            setVetoed(true)
+        si.registerPending(obj)
+        processAssociations(mongoCodecSession, entity, entityAccess, obj, 
proxyFactory, isUpdate)
+
+        if (!isUpdate) {
+            MongoCodecEntityPersister self = this
+            mongoCodecSession.addPendingInsert(new 
PendingInsertAdapter(entity, id, obj, entityAccess) {
+                @Override
+                void run() {
+                    if (!cancelInsert(entity, entityAccess)) {
+                        updateCaches(entity, obj, id)
+                        addCascadeOperation(new 
PendingOperationAdapter(entity, id, obj) {
+                            @Override
+                            void run() {
+                                self.firePostInsertEvent(entity, entityAccess)
+                            }
+                        })
+                    } else {
+                        setVetoed(true)
+                    }
+                }
+            })
+        } else {
+            mongoCodecSession.addPendingUpdate(new 
PendingUpdateAdapter(entity, id, obj, entityAccess) {
+                @Override
+                void run() {
+                    // Take snapshot of all property values BEFORE the 
PreUpdate event fires
+                    Map<String, Object> beforeUpdateSnapshot = [:]
+                    boolean hasPreExistingDirty = false
+                    if (obj instanceof DirtyCheckable) {
+                        DirtyCheckable dc = (DirtyCheckable) obj
+                        hasPreExistingDirty = dc.hasChanged() || 
!dc.listDirtyPropertyNames().isEmpty()
+                        for (PersistentProperty prop : 
entity.persistentProperties) {
+                            beforeUpdateSnapshot[prop.name] = 
entityAccess.getProperty(prop.name)
                         }
                     }
-                })
-            } else {
-                mongoCodecSession.addPendingUpdate(new 
PendingUpdateAdapter(entity, id, obj, entityAccess) {
-                    @Override
-                    void run() {
-                        // Take snapshot of all property values BEFORE the 
PreUpdate event fires
-                        Map<String, Object> beforeUpdateSnapshot = [:]
+                    if (!cancelUpdate(entity, entityAccess)) {
+                        // Compare with snapshot and mark modified properties 
dirty
                         if (obj instanceof DirtyCheckable) {
+                            DirtyCheckable dirtyCheckable = (DirtyCheckable) 
obj
+                            boolean hasNonAutoTimestampChange = 
hasPreExistingDirty
+                            List<String> onlyAutoTimestampChanged = []
                             for (PersistentProperty prop : 
entity.persistentProperties) {
-                                beforeUpdateSnapshot[prop.name] = 
entityAccess.getProperty(prop.name)
-                            }
-                        }
-                        if (!cancelUpdate(entity, entityAccess)) {
-                            // Compare with snapshot and mark modified 
properties dirty
-                            if (obj instanceof DirtyCheckable) {
-                                DirtyCheckable dirtyCheckable = 
(DirtyCheckable) obj
-                                for (PersistentProperty prop : 
entity.persistentProperties) {
-                                    Object oldValue = 
beforeUpdateSnapshot[prop.name]
-                                    Object newValue = 
entityAccess.getProperty(prop.name)
-                                    boolean valueChanged = oldValue != 
newValue && (oldValue == null || !oldValue.equals(newValue))
-                                    if (valueChanged) {
-                                        dirtyCheckable.markDirty(prop.name, 
newValue, oldValue)
+                                Object oldValue = 
beforeUpdateSnapshot[prop.name]
+                                Object newValue = 
entityAccess.getProperty(prop.name)
+                                boolean valueChanged = oldValue != newValue && 
(oldValue == null || !oldValue.equals(newValue))
+                                if (valueChanged) {
+                                    dirtyCheckable.markDirty(prop.name, 
newValue, oldValue)
+                                    if (!hasPreExistingDirty) {
+                                        String propName = prop.name
+                                        if (propName == 
GormProperties.LAST_UPDATED || propName == GormProperties.DATE_CREATED) {
+                                            
onlyAutoTimestampChanged.add(propName)
+                                        } else {
+                                            hasNonAutoTimestampChange = true
+                                        }
                                     }
                                 }
                             }
-                            updateCaches(entity, obj, id)
-                            addCascadeOperation(new 
PendingOperationAdapter(entity, id, obj) {
-                                @Override
-                                void run() {
-                                    firePostUpdateEvent(entity, entityAccess)
+                            if (!hasNonAutoTimestampChange && 
!onlyAutoTimestampChanged.isEmpty()) {
+                                // AutoTimestampEventListener set timestamp 
properties but nothing else changed.
+                                // Treat as a no-op save: reset the timestamps 
and veto the update.
+                                for (String propName in 
onlyAutoTimestampChanged) {
+                                    entityAccess.setProperty(propName, 
beforeUpdateSnapshot[propName])
                                 }
-                            })
-                        } else {
-                            setVetoed(true)
+                                dirtyCheckable.trackChanges()
+                                setVetoed(true)
+                                return
+                            }
                         }
+                        updateCaches(entity, obj, id)
+                        addCascadeOperation(new 
PendingOperationAdapter(entity, id, obj) {
+                            @Override
+                            void run() {
+                                firePostUpdateEvent(entity, entityAccess)
+                            }
+                        })
+                    } else {
+                        setVetoed(true)
                     }
-                })
-            }
+                }
+            })
         }
         return id
     }
diff --git 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy
 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy
index df651ad751..85e5fb0d07 100644
--- 
a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy
+++ 
b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy
@@ -47,7 +47,7 @@ import 
org.grails.datastore.bson.codecs.decoders.EmbeddedDecoder
 import org.grails.datastore.bson.codecs.encoders.EmbeddedCollectionEncoder
 import org.grails.datastore.bson.codecs.encoders.EmbeddedEncoder
 import org.grails.datastore.bson.codecs.encoders.IdentityEncoder
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.schemaless.DynamicAttributes
 import org.grails.datastore.mapping.collection.PersistentList
 import org.grails.datastore.mapping.collection.PersistentSet
@@ -59,12 +59,12 @@ import org.grails.datastore.mapping.core.Session
 import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
 import org.grails.datastore.mapping.dirty.checking.DirtyCheckableCollection
 import org.grails.datastore.mapping.engine.EntityAccess
-import org.grails.datastore.mapping.engine.EntityPersister
 import org.grails.datastore.mapping.engine.internal.MappingUtils
 import org.grails.datastore.mapping.model.EmbeddedPersistentEntity
 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.model.types.Basic
 import org.grails.datastore.mapping.model.types.Embedded
 import org.grails.datastore.mapping.model.types.EmbeddedCollection
 import org.grails.datastore.mapping.model.types.Identity
@@ -161,7 +161,7 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
             callback(AbstractDatastore.retrieveSession(MongoDatastore))
         }
         else {
-            GormEnhancer.findStaticApi(entity.javaClass).withSession(callback)
+            
GormRegistry.instance.findStaticApi(entity.javaClass).withSession(callback)
         }
     }
 
@@ -175,10 +175,10 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
             return cachedInstance
         }
         if (entity instanceof EmbeddedPersistentEntity) {
-            callback(AbstractDatastore.retrieveSession(MongoDatastore))
+            return callback(AbstractDatastore.retrieveSession(MongoDatastore))
         }
         else {
-            GormEnhancer.findStaticApi(entity.javaClass).withSession(callback)
+            return 
GormRegistry.instance.findStaticApi(entity.javaClass).withSession(callback)
         }
     }
 
@@ -234,6 +234,19 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
 
             def dirtyProperties = new 
ArrayList<String>(dirty.listDirtyPropertyNames())
             boolean isNew = dirtyProperties.isEmpty() && dirty.hasChanged()
+            if (!isNew && dirtyProperties.isEmpty()) {
+                // Preserve historical Mongo behavior for basic collection 
properties:
+                // a save on an entity with wrapped basic collections is 
treated as an update.
+                for (PersistentProperty prop : entity.persistentProperties) {
+                    if (prop instanceof Basic) {
+                        Object basicValue = access.getProperty(prop.name)
+                        if (basicValue instanceof DirtyCheckableCollection && 
((DirtyCheckableCollection)basicValue).hasChanged()) {
+                            isNew = true
+                            break
+                        }
+                    }
+                }
+            }
             def isVersioned = entity.isVersioned()
             if (isNew) {
                 // if it is new it can only be an embedded entity that has now 
been updated
@@ -242,14 +255,10 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
                 if (!entity.isRoot()) {
                     sets.put(MongoConstants.MONGO_CLASS_FIELD, new 
BsonString(entity.discriminator))
                 }
-
-                if (isVersioned) {
-                    EntityPersister.incrementEntityVersion(access)
-                }
-
             }
 
             for (propertyName in dirtyProperties) {
+                if (isVersioned && propertyName == entity.version.name) 
continue
                 def prop = entity.getPropertyByName(propertyName)
                 if (prop != null) {
 
@@ -291,7 +300,7 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
             }
             else {
 
-                GormEnhancer.findStaticApi(entity.javaClass).withSession { 
Session mongoSession ->
+                
GormRegistry.instance.findStaticApi(entity.javaClass).withSession { Session 
mongoSession ->
                     if (mongoSession != null) {
                         Document schemaless = (Document) 
mongoSession.getAttribute(value, SCHEMALESS_ATTRIBUTES)
                         if (schemaless != null) {
@@ -703,16 +712,39 @@ class PersistentEntityCodec extends 
BsonPersistentEntityCodec {
                 }
             }
 
+            Class associatedType = associatedEntity.javaClass
+            if (associationId != null && associatedEntity.isRoot()) {
+                try {
+                    Document raw = mongoSession.getCollection(associatedEntity)
+                            .withDocumentClass(Document)
+                            .find(new Document(MongoConstants.MONGO_ID_FIELD, 
associationId), Document)
+                            .limit(1)
+                            .first()
+                    if (raw != null) {
+                        Object discriminator = 
raw.get(MongoConstants.MONGO_CLASS_FIELD)
+                        if (discriminator != null) {
+                            PersistentEntity childEntity = 
associatedEntity.mappingContext
+                                    
.getChildEntityByDiscriminator(associatedEntity.rootEntity, 
discriminator.toString())
+                            if (childEntity != null) {
+                                associatedType = childEntity.javaClass
+                            }
+                        }
+                    }
+                } catch (Exception ignored) {
+                    // fall back to the declared association type
+                }
+            }
+
             if (isLazy) {
                 entityAccess.setPropertyNoConversion(
                         property.name,
-                        mongoSession.proxy(associatedEntity.javaClass, 
associationId)
+                        mongoSession.proxy(associatedType, associationId)
                 )
             }
             else {
                 entityAccess.setPropertyNoConversion(
                         property.name,
-                        mongoSession.retrieve(associatedEntity.javaClass, 
associationId)
+                        mongoSession.retrieve(associatedType, associationId)
                 )
             }
 
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy
index 0737fcb83a..ec0c73a2fc 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy
@@ -25,10 +25,11 @@ import grails.core.GrailsApplication
 import grails.gorm.validation.PersistentEntityValidator
 import groovy.util.logging.Slf4j
 import org.apache.grails.data.testing.tck.base.GrailsDataTckManager
+import spock.lang.Specification
 import org.apache.grails.testing.mongo.AbstractMongoGrailsExtension
 import org.bson.Document
 import org.grails.datastore.bson.query.BsonQuery
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.gorm.mongo.Birthday
 import 
org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator
 import 
org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry
@@ -64,6 +65,13 @@ class GrailsDataMongoTckManager extends GrailsDataTckManager 
{
     MongoDatastore multiDataSourceDatastore
     MongoDatastore multiTenantMultiDataSourceDatastore
 
+    @Override
+    void setup(Class<? extends Specification> spec) {
+        cleanRegistry()
+        GormRegistry.reset()
+        super.setup(spec)
+    }
+
     @Override
     void setupSpec() {
         super.setupSpec()
@@ -75,7 +83,9 @@ class GrailsDataMongoTckManager extends GrailsDataTckManager {
                 (MongoSettings.SETTING_DATABASE_NAME): 'test',
                 (MongoSettings.SETTING_HOST)         : mongoDBContainer.host,
                 (MongoSettings.SETTING_PORT)         : 
mongoDBContainer.getMappedPort(AbstractMongoGrailsExtension.DEFAULT_MONGO_PORT) 
as String,
-                //TODO: 'grails.mongodb.url': "mongodb://${host}:${port as 
String}/myDb" as String
+                'grails.mongodb.connections': [
+                        'secondary': ['url': 
"mongodb://${mongoDBContainer.host}:${mongoDBContainer.getMappedPort(AbstractMongoGrailsExtension.DEFAULT_MONGO_PORT)}/tckSecondaryDB"
 as String]
+                ]
         ]
     }
 
@@ -147,7 +157,7 @@ class GrailsDataMongoTckManager extends 
GrailsDataTckManager {
                         }
                     }
             for (cls in domainClasses) {
-                GormEnhancer.findValidationApi(cls).validator = null
+                GormRegistry.instance.findValidationApi(cls).validator = null
             }
         }
         finally {
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy
index 5dae9666d0..f5143a0403 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy
@@ -62,7 +62,7 @@ class DirtyCheckUpdateSpec extends 
GrailsDataTckSpec<GrailsDataMongoTckManager>
         b = Bar.get(b.id)
 
         then:
-        b.version == 3 //should be 2
+        b.version == 1
     }
 
     void "Test that the version is incremented on save"() {
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy
new file mode 100644
index 0000000000..d3c8fe81d2
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy
@@ -0,0 +1,223 @@
+/*
+ *  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.mongo
+
+import grails.gorm.MultiTenant
+import grails.gorm.annotation.Entity
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
+import 
org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import spock.lang.Shared
+import spock.lang.Specification
+
+/**
+ * Verifies the O(M+N) memory guarantee of {@link GormRegistry} in the MongoDB
+ * context.
+ *
+ * The registry must satisfy:
+ *   - O(M)   static/instance/validation API maps — one entry per entity 
class, never per tenant
+ *   - O(N)   datastoresByQualifier map — one entry per tenant/qualifier
+ *   - O(1)   API retrieval for any qualifier — same singleton instance 
returned
+ *
+ * where M = number of entity classes, N = number of tenants/connections.
+ */
+class GormRegistryScalabilitySpec extends Specification {
+
+    static final int TENANT_COUNT = 5
+
+    @Shared MongoDatastore datastore
+
+    void setupSpec() {
+        System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "")
+        Map config = [
+            "grails.mongodb.multiTenancy.mode"              : "DATABASE",
+            "grails.mongodb.multiTenancy.tenantResolverClass": 
ScalabilityTenantsResolver,
+            "grails.mongodb.databaseName": "scalabilityDB"
+        ]
+        datastore = new MongoDatastore(
+            DatastoreUtils.createPropertyResolver(config),
+            ScalabilityBook, ScalabilityAuthor
+        )
+    }
+
+    void cleanupSpec() {
+        datastore?.close()
+        System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+    }
+
+    // 
-------------------------------------------------------------------------
+    // O(M) — API maps must have exactly one entry per entity class, not per 
tenant
+    // 
-------------------------------------------------------------------------
+
+    void "GormRegistry staticApis map size equals number of entity classes 
(O(M))"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+
+        expect: "one static API entry per entity — never multiplied by tenant 
count"
+        registry.staticApiRegistry.containsKey(ScalabilityBook.name)
+        registry.staticApiRegistry.containsKey(ScalabilityAuthor.name)
+
+        and: "our two entities contribute exactly 2 keys (not 2 × tenant 
count)"
+        registry.staticApiRegistry.keySet().count { it == ScalabilityBook.name 
|| it == ScalabilityAuthor.name } == 2
+    }
+
+    void "GormRegistry instanceApis map size equals number of entity classes 
(O(M))"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+
+        expect:
+        registry.instanceApiRegistry.containsKey(ScalabilityBook.name)
+        registry.instanceApiRegistry.containsKey(ScalabilityAuthor.name)
+
+        and: "our two entities contribute exactly 2 keys (not 2 × tenant 
count)"
+        registry.instanceApiRegistry.keySet().count { it == 
ScalabilityBook.name || it == ScalabilityAuthor.name } == 2
+    }
+
+    void "GormRegistry validationApis map size equals number of entity classes 
(O(M))"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+
+        expect:
+        registry.validationApiRegistry.containsKey(ScalabilityBook.name)
+        registry.validationApiRegistry.containsKey(ScalabilityAuthor.name)
+
+        and: "our two entities contribute exactly 2 keys (not 2 × tenant 
count)"
+        registry.validationApiRegistry.keySet().count { it == 
ScalabilityBook.name || it == ScalabilityAuthor.name } == 2
+    }
+
+    // 
-------------------------------------------------------------------------
+    // O(1) — same API singleton returned regardless of qualifier
+    // 
-------------------------------------------------------------------------
+
+    void "getStaticApi returns the same singleton instance for any qualifier 
(O(1) retrieval)"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+        GormStaticApi defaultApi = registry.getStaticApi(ScalabilityBook.name)
+
+        expect: "default qualifier retrieves the canonical singleton"
+        defaultApi != null
+
+        and: "retrieval remains O(1) and returns the same singleton regardless 
of tenant loop context"
+        ScalabilityTenantsResolver.TENANTS.every { tenantId ->
+            registry.getStaticApi(ScalabilityBook.name).is(defaultApi)
+        }
+    }
+
+    void "getInstanceApi returns the same singleton instance for any qualifier 
(O(1) retrieval)"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+        GormInstanceApi defaultApi = 
registry.getInstanceApi(ScalabilityAuthor.name)
+
+        expect:
+        defaultApi != null
+        ScalabilityTenantsResolver.TENANTS.every { tenantId ->
+            registry.getInstanceApi(ScalabilityAuthor.name).is(defaultApi)
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    // O(N) — qualifier map must grow with tenants (datastoresByQualifier)
+    // 
-------------------------------------------------------------------------
+
+    void "datastoresByQualifier contains all registered tenants (O(N) 
qualifier map)"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+
+        expect: "at minimum, the default qualifier is registered"
+        registry.datastoresByQualifier.containsKey(ConnectionSource.DEFAULT)
+
+        and: "the qualifier map has at least one entry (the parent datastore)"
+        registry.datastoresByQualifier.size() >= 1
+    }
+
+    // 
-------------------------------------------------------------------------
+    // No spurious entries — unknown qualifiers must not pollute the registry
+    // 
-------------------------------------------------------------------------
+
+    void "looking up an unknown qualifier does not create a spurious registry 
entry"() {
+        given:
+        GormRegistry registry = GormRegistry.instance
+        String ghost = "ghost_tenant_" + System.currentTimeMillis()
+        int sizeBefore = registry.datastoresByQualifier.size()
+
+        when:
+        def result = registry.getDatastore(ScalabilityBook.name, ghost)
+
+        then: "nothing is found"
+        result == null
+
+        and: "the map size is unchanged — no null/empty entry was inserted"
+        registry.datastoresByQualifier.size() == sizeBefore
+    }
+
+    void "datastore deregisters from GormRegistry on close"() {
+        given: "a temporary datastore registered in GormRegistry"
+        def tempDatastore = new MongoDatastore(
+            DatastoreUtils.createPropertyResolver([
+                "grails.mongodb.databaseName": "tempScalabilityDB"
+            ]),
+            ScalabilityBook
+        )
+        GormRegistry registry = GormRegistry.instance
+
+        expect: "the datastore is registered"
+        registry.allDatastores.contains(tempDatastore)
+
+        when: "the datastore is closed"
+        tempDatastore.close()
+
+        then: "the datastore is removed from the GormRegistry"
+        !registry.allDatastores.contains(tempDatastore)
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Test fixtures
+// ---------------------------------------------------------------------------
+
+class ScalabilityTenantsResolver implements AllTenantsResolver {
+    static final List<String> TENANTS = ["dbA", "dbB", "dbC", "dbD", "dbE"]
+
+    @Override
+    Serializable resolveTenantIdentifier() {
+        TENANTS[0]
+    }
+
+    @Override
+    Iterable<Serializable> resolveTenantIds() {
+        TENANTS
+    }
+}
+
+@Entity
+class ScalabilityBook implements MultiTenant<ScalabilityBook> {
+    String title
+    String author
+}
+
+@Entity
+class ScalabilityAuthor implements MultiTenant<ScalabilityAuthor> {
+    String name
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy
index e6f56b7f6f..1f718d8188 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy
@@ -32,7 +32,7 @@ class IsNullSpec extends 
GrailsDataTckSpec<GrailsDataMongoTckManager> {
     @Issue('GPMONGODB-164')
     void "Test isNull works in a criteria query"() {
         given: "Some test data"
-        new Elephant(name: "Dumbo").save(validate: false)
+        new Elephant(name: "Dumbo").save(flush: true, validate: false)
         new Elephant(name: "Big Daddy", trunk: new Trunk(length: 
10).save()).save(flush: true, validate: false)
         manager.session.clear()
 
@@ -58,7 +58,7 @@ class IsNullSpec extends 
GrailsDataTckSpec<GrailsDataMongoTckManager> {
     @Issue('GPMONGODB-164')
     void "Test isNull works in a dynamic finder"() {
         given: "Some test data"
-        new Elephant(name: "Dumbo").save(validate: false)
+        new Elephant(name: "Dumbo").save(flush: true, validate: false)
         new Elephant(name: "Big Daddy", trunk: new Trunk(length: 
10).save()).save(flush: true, validate: false)
         manager.session.clear()
 
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy
new file mode 100644
index 0000000000..a3c4df4646
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy
@@ -0,0 +1,117 @@
+/*
+ *  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.mongo
+
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.mongo.api.MongoStaticApi
+import org.grails.datastore.mapping.model.MappingContext
+import spock.lang.Specification
+
+/**
+ * Tests for MongoGormApiFactory
+ */
+class MongoGormApiFactorySpec extends Specification {
+
+    void 'createStaticApi returns MongoStaticApi instance'() {
+        given:
+        MongoGormApiFactory factory = new MongoGormApiFactory()
+        MappingContext mappingContext = Mock(MappingContext)
+        DatastoreResolver resolver = Stub(DatastoreResolver)
+        String qualifier = 'default'
+
+        when:
+        def staticApi = factory.createStaticApi(
+            TestEntity,
+            mappingContext,
+            resolver,
+            qualifier,
+            GormRegistry.instance
+        )
+
+        then:
+        staticApi != null
+        staticApi instanceof MongoStaticApi
+        staticApi.persistentClass == TestEntity
+    }
+
+    void 'createStaticApi creates finders'() {
+        given:
+        MongoGormApiFactory factory = new MongoGormApiFactory()
+        MappingContext mappingContext = Mock(MappingContext)
+        DatastoreResolver resolver = Stub(DatastoreResolver)
+
+        when:
+        def staticApi = factory.createStaticApi(
+            TestEntity,
+            mappingContext,
+            resolver,
+            'default',
+            GormRegistry.instance
+        )
+
+        then:
+        staticApi.finders.size() > 0
+    }
+
+    void 'createInstanceApi uses parent factory behavior'() {
+        given:
+        MongoGormApiFactory factory = new MongoGormApiFactory()
+        MappingContext mappingContext = Mock(MappingContext)
+        DatastoreResolver resolver = Stub(DatastoreResolver)
+
+        when:
+        def instanceApi = factory.createInstanceApi(
+            TestEntity,
+            mappingContext,
+            resolver,
+            GormRegistry.instance,
+            true,
+            false
+        )
+
+        then:
+        instanceApi != null
+        instanceApi.failOnError
+        !instanceApi.markDirty
+    }
+
+    void 'createValidationApi uses parent factory behavior'() {
+        given:
+        MongoGormApiFactory factory = new MongoGormApiFactory()
+        MappingContext mappingContext = Mock(MappingContext)
+        DatastoreResolver resolver = Stub(DatastoreResolver)
+
+        when:
+        def validationApi = factory.createValidationApi(
+            TestEntity,
+            mappingContext,
+            resolver,
+            GormRegistry.instance
+        )
+
+        then:
+        validationApi != null
+    }
+
+    static class TestEntity {
+        String name
+        Integer age
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy
new file mode 100644
index 0000000000..1d072d884b
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy
@@ -0,0 +1,101 @@
+/*
+ *  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.mongo
+
+import groovy.transform.CompileDynamic
+import org.grails.datastore.gorm.mongo.api.MongoGormInstanceApi
+import org.grails.datastore.gorm.mongo.transactions.MongoTransactionContext
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.grails.datastore.mapping.mongo.config.MongoMappingContext
+import spock.lang.Specification
+
+/**
+ * Specification for MongoGormInstanceApi
+ *
+ * @author Graeme Rocher
+ * @since 8.0
+ */
+@CompileDynamic
+class MongoGormInstanceApiSpec extends Specification {
+    private MongoDatastore datastore
+
+    void "auto-flush gate defaults to enabled outside rollback-aware 
context"() {
+        given:
+        def api = newApi()
+
+        expect:
+        api.exposedShouldAutoFlushByDefault()
+    }
+
+    void "auto-flush gate is disabled inside rollback-aware context only"() {
+        given:
+        def api = newApi()
+
+        expect:
+        api.exposedShouldAutoFlushByDefault()
+
+        when:
+        def insideGate = MongoTransactionContext.withRollbackAware {
+            api.exposedShouldAutoFlushByDefault()
+        }
+
+        then:
+        !insideGate
+        api.exposedShouldAutoFlushByDefault()
+    }
+
+    void "auto-flush gate handles nested rollback-aware contexts and restores 
state"() {
+        given:
+        def api = newApi()
+
+        when:
+        def outer = MongoTransactionContext.withRollbackAware {
+            def inner = MongoTransactionContext.withRollbackAware {
+                api.exposedShouldAutoFlushByDefault()
+            }
+            [api.exposedShouldAutoFlushByDefault(), inner]
+        }
+
+        then:
+        !outer[0]
+        !outer[1]
+        api.exposedShouldAutoFlushByDefault()
+    }
+
+    private TestableMongoGormInstanceApi newApi() {
+        datastore = new MongoDatastore(new MongoMappingContext('GateEntity'))
+        new TestableMongoGormInstanceApi(datastore)
+    }
+
+    void cleanup() {
+        datastore?.close()
+    }
+
+    @CompileDynamic
+    private static class TestableMongoGormInstanceApi extends 
MongoGormInstanceApi<Object> {
+        TestableMongoGormInstanceApi(MongoDatastore datastore) {
+            super(Object, datastore)
+        }
+
+        boolean exposedShouldAutoFlushByDefault() {
+            shouldAutoFlushByDefault()
+        }
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy
new file mode 100644
index 0000000000..99d3a64ecd
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy
@@ -0,0 +1,72 @@
+/*
+ *  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.mongo
+
+import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager
+import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
+import grails.mongodb.geo.*
+import grails.persistence.Entity
+
+@Entity
+class PlacePartial {
+    Long id
+    String name
+    Point point
+    Polygon polygon
+    LineString lineString
+    Box box
+    Circle circle
+    Sphere sphere
+    MultiPoint multiPoint
+    MultiLineString multiLineString
+    MultiPolygon multiPolygon
+    GeometryCollection geometryCollection
+    
+    static mapping = {
+        point geoIndex: '2dsphere'
+    }
+}
+
+class PlacePartialTest extends GrailsDataTckSpec<GrailsDataMongoTckManager> {
+    void setupSpec() {
+        manager.registerDomainClasses(PlacePartial)
+    }
+    
+    void "test place with only one field"() {
+        when:
+        def col = new GeometryCollection()
+        col << Point.valueOf(5, 10)
+        def p = new PlacePartial(geometryCollection: col)
+        println "Saving with only geometryCollection set..."
+        p.save(flush: true, validate: false)
+        println "Saved with id: ${p.id}"
+        manager.session.clear()
+        
+        then:
+        p.id != null
+        
+        when:
+        println "Retrieving..."
+        p = PlacePartial.get(p.id)
+        println "Retrieved: ${p}"
+        
+        then:
+        p != null
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy
new file mode 100644
index 0000000000..c95fc4f6e2
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy
@@ -0,0 +1,69 @@
+/*
+ *  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.mongo
+
+import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager
+import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
+import grails.mongodb.geo.*
+import grails.persistence.Entity
+
+@Entity
+class PlaceException {
+    Long id
+    String name
+    Point point
+    Polygon polygon
+    
+    static mapping = {
+        point geoIndex: '2dsphere'
+    }
+}
+
+class PlaceWithExceptionTest extends 
GrailsDataTckSpec<GrailsDataMongoTckManager> {
+    void setupSpec() {
+        manager.registerDomainClasses(PlaceException)
+    }
+    
+    void "test place with exception handling"() {
+        when:
+        def col = new GeometryCollection()
+        col << Point.valueOf(5, 10)
+        def p = new PlaceException(name: "Test")  // Don't set any GeoJSON 
fields
+        p.save(flush: true, validate: false)
+        println "Saved with id: ${p.id}"
+        manager.session.clear()
+        
+        then:
+        p.id != null
+        
+        when:
+        try {
+            p = PlaceException.get(p.id)
+            println "Retrieved successfully: ${p}"
+        } catch (Exception e) {
+            println "Exception during get: ${e.class.name}"
+            println "Message: ${e.message}"
+            e.printStackTrace(System.out)
+            throw e
+        }
+        
+        then:
+        p != null
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy
new file mode 100644
index 0000000000..cb255a892b
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy
@@ -0,0 +1,69 @@
+/*
+ *  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.mongo
+
+import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager
+import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
+import grails.mongodb.geo.*
+import grails.persistence.Entity
+
+@Entity
+class PlaceNS {
+    Long id
+    String name
+    Point point
+    Polygon polygon
+    LineString lineString
+    Box box
+    Circle circle
+    // NO Sphere!
+    MultiPoint multiPoint
+    MultiLineString multiLineString
+    MultiPolygon multiPolygon
+    GeometryCollection geometryCollection
+    
+    static mapping = {
+        point geoIndex: '2dsphere'
+    }
+}
+
+class PlaceWithoutSphereTest extends 
GrailsDataTckSpec<GrailsDataMongoTckManager> {
+    void setupSpec() {
+        manager.registerDomainClasses(PlaceNS)
+    }
+    
+    void "test place without sphere"() {
+        given:
+        def point = new Point(5, 10)
+        def poly = Polygon.valueOf([[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], 
[100.0, 1.0], [100.0, 0.0]])
+        def line = LineString.valueOf([[100.0, 0.0], [101.0, 1.0]])
+        def box = Box.valueOf([[0, 0], [10, 10]])
+        def circle = Circle.valueOf([[5, 5], 3])
+        
+        when:
+        def p = new PlaceNS(point: point, polygon: poly, lineString: line, 
box: box, circle: circle)
+        p.save(flush: true, validate: false)
+        manager.session.clear()
+        p = PlaceNS.get(p.id)
+        
+        then:
+        p != null
+        p.point == point
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy
index e47a9d0f68..90a6319718 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy
@@ -107,4 +107,3 @@ class Chapter implements Serializable {
 
     String title
 }
-
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy
new file mode 100644
index 0000000000..210b623945
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy
@@ -0,0 +1,151 @@
+/*
+ *  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.mongo.api
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.Tenants
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.multitenancy.TenantDelegatingGormOperations
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import org.grails.datastore.mapping.model.MappingContext
+import org.bson.Document
+import spock.lang.Specification
+
+class MongoTenantContextProfilingSpec extends Specification {
+
+    void setup() {
+        GormRegistry.instance.reset()
+    }
+
+    void cleanup() {
+        GormRegistry.instance.reset()
+    }
+
+    void "profile mongo tenant wrapping overhead"() {
+        given:
+        def mappingContext = Stub(MappingContext)
+        def datastore = Stub(MultiTenantCapableDatastore) {
+            getMultiTenancyMode() >> 
MultiTenancySettings.MultiTenancyMode.DATABASE
+            getMappingContext() >> mappingContext
+            getDatastoreForTenantId(_) >> { return it[0] == null ? delegate : 
delegate }
+        }
+        
+        def registry = GormRegistry.instance
+        registry.registerDatastore("default", datastore)
+        
+        def persistentEntity = 
Stub(org.grails.datastore.mapping.model.PersistentEntity) {
+            isMultiTenant() >> true
+            getTenantId() >> 
Stub(org.grails.datastore.mapping.model.PersistentProperty) {
+                getName() >> "tenantId"
+            }
+        }
+        
+        def staticApi = new DummyMongoStaticApi(TenantEntity, mappingContext, 
datastore, persistentEntity)
+        def ops = new TenantDelegatingGormOperations<TenantEntity>((Datastore) 
datastore, "tenant1", staticApi)
+        def qualifiedApi = staticApi.forQualifier("tenant1")
+        
+        int iterations = 1000
+
+        when: "Calling operations repeatedly via 
TenantDelegatingGormOperations (wrapped every time)"
+        long startWrapped = System.currentTimeMillis()
+        for (int i = 0; i < iterations; i++) {
+            ops.exists(1L)
+        }
+        long endWrapped = System.currentTimeMillis()
+
+        and: "Calling operations via qualified API (unwrapped, but pre-bound)"
+        long startQualified = System.currentTimeMillis()
+        for (int i = 0; i < iterations; i++) {
+            qualifiedApi.exists(1L)
+        }
+        long endQualified = System.currentTimeMillis()
+
+        and: "Calling operations via closure block (wrapped once)"
+        long startBlock = System.currentTimeMillis()
+        Tenants.withId((MultiTenantCapableDatastore) datastore, "tenant1") {
+            for (int i = 0; i < iterations; i++) {
+                staticApi.exists(1L)
+            }
+        }
+        long endBlock = System.currentTimeMillis()
+
+        and: "Calling internal wrapping logic directly (wrapped vs pre-bound)"
+        def filter = new Document()
+        long startInternalWrapped = System.currentTimeMillis()
+        Tenants.withId((MultiTenantCapableDatastore) datastore, "tenant1") {
+            for (int i = 0; i < iterations; i++) {
+                staticApi.wrapFilterWithMultiTenancy(filter)
+            }
+        }
+        long endInternalWrapped = System.currentTimeMillis()
+
+        long startInternalPrebound = System.currentTimeMillis()
+        for (int i = 0; i < iterations; i++) {
+            qualifiedApi.wrapFilterWithMultiTenancy(filter)
+        }
+        long endInternalPrebound = System.currentTimeMillis()
+
+        then:
+        println "Mongo Single block wrapped operations: ${endBlock - 
startBlock} ms"
+        println "Mongo Qualified API operations: ${endQualified - 
startQualified} ms"
+        println "Mongo Per-method wrapped operations: ${endWrapped - 
startWrapped} ms"
+        println "Mongo Internal wrapFilter (wrapped): ${endInternalWrapped - 
startInternalWrapped} ms"
+        println "Mongo Internal wrapFilter (pre-bound): ${endInternalPrebound 
- startInternalPrebound} ms"
+        
+        true
+    }
+
+    static class TenantEntity implements MultiTenant<TenantEntity> {
+        Long id
+    }
+
+    static class DummyMongoStaticApi extends MongoStaticApi<TenantEntity> {
+        private final org.grails.datastore.mapping.model.PersistentEntity 
persistentEntityStub
+
+        DummyMongoStaticApi(Class<TenantEntity> persistentClass, 
MappingContext mappingContext, MultiTenantCapableDatastore datastore, 
org.grails.datastore.mapping.model.PersistentEntity persistentEntityStub, 
String qualifier = "default") {
+            super(persistentClass, mappingContext, [], new 
org.grails.datastore.gorm.DatastoreResolver() {
+                @Override org.grails.datastore.mapping.core.Datastore 
resolve() { return (Datastore) datastore }
+            }, qualifier)
+            this.persistentEntityStub = persistentEntityStub
+        }
+
+        @Override
+        boolean exists(Serializable id) {
+            return true
+        }
+
+        @Override
+        org.grails.datastore.gorm.GormStaticApi<TenantEntity> 
forQualifier(String qualifier) {
+            return new DummyMongoStaticApi(persistentClass, mappingContext, 
(MultiTenantCapableDatastore)datastore, persistentEntityStub, qualifier)
+        }
+
+        @Override
+        public org.bson.conversions.Bson 
wrapFilterWithMultiTenancy(org.bson.conversions.Bson filter) {
+            return super.wrapFilterWithMultiTenancy(filter)
+        }
+
+        @Override
+        org.grails.datastore.mapping.model.PersistentEntity 
getGormPersistentEntity() {
+            return persistentEntityStub
+        }
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy
index 429c575374..6525099533 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy
@@ -41,16 +41,15 @@ import static com.mongodb.client.model.Filters.*
 @RestoreSystemProperties
 class MultiTenancySpec extends AutoStartedMongoSpec {
 
-    @AutoCleanup MongoDatastore datastore
+    @Shared @AutoCleanup MongoDatastore datastore
 
     @Override
     boolean shouldInitializeDatastore() {
         false
     }
 
-    void setup() {
-        // Ensure tenant property is cleared before each test for test 
isolation
-        System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+    void setupSpec() {
+        org.grails.datastore.gorm.GormRegistry.reset()
         Map config = [
                 "grails.gorm.multiTenancy.mode"               :"DISCRIMINATOR",
                 "grails.gorm.multiTenancy.tenantResolverClass": MyResolver,
@@ -59,6 +58,11 @@ class MultiTenancySpec extends AutoStartedMongoSpec {
         this.datastore = new MongoDatastore(config, getDomainClasses() as 
Class[])
     }
 
+    void setup() {
+        // Ensure tenant property is cleared before each test for test 
isolation
+        System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+    }
+
 
     void "Test persist and retrieve entities with multi tenancy"() {
         setup:
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy
index 406b967d7d..6420f24d85 100644
--- 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy
@@ -35,16 +35,15 @@ import spock.lang.Shared
 @RestoreSystemProperties
 class SchemaBasedMultiTenancySpec extends AutoStartedMongoSpec {
 
-    @AutoCleanup MongoDatastore datastore
+    @Shared @AutoCleanup MongoDatastore datastore
 
     @Override
     boolean shouldInitializeDatastore() {
         false
     }
 
-    void setup() {
-        // Ensure tenant property is cleared before each test for test 
isolation
-        System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+    void setupSpec() {
+        org.grails.datastore.gorm.GormRegistry.reset()
         Map config = [
                 (MongoSettings.SETTING_URL): 
"mongodb://${mongoHost}:${mongoPort}/defaultDb" as String,
                 "grails.gorm.multiTenancy.mode"               :"SCHEMA",
@@ -53,6 +52,11 @@ class SchemaBasedMultiTenancySpec extends 
AutoStartedMongoSpec {
         this.datastore = new MongoDatastore(config, getDomainClasses() as 
Class[])
     }
 
+    void setup() {
+        // Ensure tenant property is cleared before each test for test 
isolation
+        System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+    }
+
     void "Test no tenant id"() {
         when:
         CompanyB.DB
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy
new file mode 100644
index 0000000000..e898671362
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy
@@ -0,0 +1,116 @@
+/*
+ *  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.mongo.transactions
+
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.grails.datastore.mapping.mongo.config.MongoMappingContext
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.DefaultTransactionAttribute
+import spock.lang.Specification
+
+/**
+ * Specification for MongoGormTransactionTemplate
+ */
+class MongoGormTransactionTemplateSpec extends Specification {
+    void "MongoTransactionContext enables and restores rollback-aware 
marker"() {
+        expect:
+        !MongoTransactionContext.isRollbackAwareActive()
+
+        when:
+        def inside = MongoTransactionContext.withRollbackAware {
+            MongoTransactionContext.isRollbackAwareActive()
+        }
+
+        then:
+        inside
+        !MongoTransactionContext.isRollbackAwareActive()
+    }
+
+    void "MongoTransactionContext supports nested scopes"() {
+        when:
+        def result = MongoTransactionContext.withRollbackAware {
+            def nested = MongoTransactionContext.withRollbackAware {
+                MongoTransactionContext.isRollbackAwareActive()
+            }
+            [MongoTransactionContext.isRollbackAwareActive(), nested]
+        }
+
+        then:
+        result[0]
+        result[1]
+        !MongoTransactionContext.isRollbackAwareActive()
+    }
+
+    void "MongoGormTransactionTemplate can be instantiated with 
TransactionManager"() {
+        given: "a mock datastore and transaction manager"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+
+        when: "creating MongoGormTransactionTemplate"
+        def template = new MongoGormTransactionTemplate(datastore, 
mockTxManager)
+
+        then: "instance is created successfully"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+
+    void "MongoGormTransactionTemplate can be instantiated with 
TransactionDefinition"() {
+        given: "mock objects"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def mockDefinition = Mock(TransactionDefinition) {
+            getIsolationLevel() >> TransactionDefinition.ISOLATION_DEFAULT
+            getPropagationBehavior() >> 
TransactionDefinition.PROPAGATION_REQUIRED
+            getTimeout() >> -1
+            isReadOnly() >> false
+        }
+
+        when: "creating MongoGormTransactionTemplate with definition"
+        def template = new MongoGormTransactionTemplate(datastore, 
mockTxManager, mockDefinition)
+
+        then: "instance is created successfully"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+
+    void "MongoGormTransactionTemplate can be instantiated with 
TransactionAttribute"() {
+        given: "mock objects"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def attribute = new DefaultTransactionAttribute()
+
+        when: "creating MongoGormTransactionTemplate with attribute"
+        def template = new MongoGormTransactionTemplate(datastore, 
mockTxManager, attribute)
+
+        then: "instance is created successfully"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+}
diff --git 
a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy
 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy
new file mode 100644
index 0000000000..daf7497a67
--- /dev/null
+++ 
b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy
@@ -0,0 +1,112 @@
+/*
+ *  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.mongo.transactions
+
+import grails.gorm.transactions.GrailsTransactionTemplate
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.grails.datastore.mapping.mongo.config.MongoMappingContext
+import org.springframework.transaction.PlatformTransactionManager
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.interceptor.DefaultTransactionAttribute
+import spock.lang.Specification
+
+/**
+ * Specification for MongoTransactionTemplateFactory
+ */
+class MongoTransactionTemplateFactorySpec extends Specification {
+
+    void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate 
with default settings"() {
+        given: "a mock datastore and transaction manager"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def factory = new MongoTransactionTemplateFactory(datastore)
+
+        when: "creating transaction template"
+        def template = factory.createTransactionTemplate(mockTxManager)
+
+        then: "MongoGormTransactionTemplate is returned"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+        template instanceof GrailsTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+
+    void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate 
with TransactionDefinition"() {
+        given: "mock objects"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def mockDefinition = Mock(TransactionDefinition) {
+            getIsolationLevel() >> TransactionDefinition.ISOLATION_DEFAULT
+            getPropagationBehavior() >> 
TransactionDefinition.PROPAGATION_REQUIRED
+            getTimeout() >> -1
+            isReadOnly() >> false
+        }
+        def factory = new MongoTransactionTemplateFactory(datastore)
+
+        when: "creating transaction template with definition"
+        def template = factory.createTransactionTemplate(mockTxManager, 
mockDefinition)
+
+        then: "MongoGormTransactionTemplate is returned"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+
+    void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate 
with TransactionAttribute"() {
+        given: "mock objects"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def attribute = new DefaultTransactionAttribute()
+        def factory = new MongoTransactionTemplateFactory(datastore)
+
+        when: "creating transaction template with attribute"
+        def template = factory.createTransactionTemplate(mockTxManager, 
attribute)
+
+        then: "MongoGormTransactionTemplate is returned"
+        template != null
+        template instanceof MongoGormTransactionTemplate
+
+        cleanup:
+        datastore.close()
+    }
+
+    void "MongoTransactionTemplateFactory is consistent across calls"() {
+        given: "a factory and transaction manager"
+        def datastore = new MongoDatastore(new MongoMappingContext('TxEntity'))
+        def mockTxManager = Mock(PlatformTransactionManager)
+        def factory = new MongoTransactionTemplateFactory(datastore)
+
+        when: "creating multiple templates"
+        def template1 = factory.createTransactionTemplate(mockTxManager)
+        def template2 = factory.createTransactionTemplate(mockTxManager)
+
+        then: "both are MongoGormTransactionTemplate instances"
+        template1 instanceof MongoGormTransactionTemplate
+        template2 instanceof MongoGormTransactionTemplate
+        template1.class == template2.class
+
+        cleanup:
+        datastore.close()
+    }
+}
diff --git a/grails-data-mongodb/docs/build.gradle 
b/grails-data-mongodb/docs/build.gradle
index a4c2f4b59b..3696616249 100644
--- a/grails-data-mongodb/docs/build.gradle
+++ b/grails-data-mongodb/docs/build.gradle
@@ -51,7 +51,7 @@ tasks.register('resolveMongodbVersion').configure { Task 
docTask ->
 dependencies {
     documentation platform(project(':grails-bom'))
     documentation 'org.fusesource.jansi:jansi'
-    documentation 'jline:jline'
+    documentation 'jline:jline:2.14.6'
     documentation 'org.apache.groovy:groovy'
     documentation 'org.apache.groovy:groovy-ant'
     documentation 'org.apache.groovy:groovy-groovydoc'
diff --git 
a/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy
 
b/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy
index c1b4b9e00c..8abea4748c 100644
--- 
a/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy
+++ 
b/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy
@@ -49,7 +49,7 @@ import org.bson.Document
 import org.bson.conversions.Bson
 import org.bson.types.ObjectId
 
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
 import org.grails.datastore.mapping.mongo.AbstractMongoSession
 import org.grails.datastore.mapping.mongo.MongoConstants
 import 
org.grails.datastore.mapping.mongo.engine.AbstractMongoObectEntityPersister
@@ -74,7 +74,7 @@ class MongoExtensions {
             return (T) document
         }
         else {
-            def datastore = GormEnhancer.findDatastore(cls)
+            def datastore = 
GormRegistry.instance.apiResolver.findDatastore(cls)
             AbstractMongoSession session = (AbstractMongoSession) 
datastore.currentSession
             if (session != null) {
                 return session.decode(cls, document)
@@ -93,7 +93,7 @@ class MongoExtensions {
             return (T) iterable
         }
         else {
-            def datastore = GormEnhancer.findDatastore(cls)
+            def datastore = 
GormRegistry.instance.apiResolver.findDatastore(cls)
             AbstractMongoSession session = (AbstractMongoSession) 
datastore.currentSession
 
             if (session != null) {
@@ -106,7 +106,7 @@ class MongoExtensions {
     }
 
     static <T> List<T> toList(FindIterable iterable, Class<T> cls) {
-        def datastore = GormEnhancer.findDatastore(cls)
+        def datastore = GormRegistry.instance.apiResolver.findDatastore(cls)
         AbstractMongoSession session = (AbstractMongoSession) 
datastore.currentSession
 
         MongoEntityPersister p = (MongoEntityPersister) 
session.getPersister(cls)
@@ -620,4 +620,3 @@ class MongoExtensions {
     }
 
 }
-

Reply via email to