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

borinquenkid pushed a commit to branch 8.0.x-hibernate7.gorm-scaling-clean
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 949fdaaeee9a94351c4f3bf8a020c4f4ecdec009
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Thu May 21 18:20:53 2026 -0500

    Add grails-datastore-core optimization changes
    
    Includes SessionResolver and ThreadLocalSessionResolver (new 
interfaces/classes
    introduced by the O(M+N) scaling refactor), plus updates to 
AbstractDatastore,
    AbstractMappingContext, and related core classes that the datastore modules
    (SimpleMap, Hibernate 5/7) depend on at compile time.
    
    Missed from initial clean rebuild commit.
    
    Agent collaboration note: Claude Sonnet 4.6 assisted; borinquenkid is the
    primary author and remains responsible for the final changes.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../datastore/mapping/core/AbstractDatastore.java  | 80 +++++++++++++++++-
 .../grails/datastore/mapping/core/Datastore.java   |  5 ++
 .../datastore/mapping/core/DatastoreUtils.java     | 64 +++++++++++++--
 .../Service.groovy => core/SessionResolver.groovy} | 36 ++++----
 .../mapping/core/ThreadLocalSessionResolver.groovy | 66 +++++++++++++++
 .../AbstractConnectionSourceFactory.java           | 11 +++
 .../ConnectionSourceSettingsBuilder.groovy         |  4 +
 .../dirty/checking/DirtyCheckingSupport.groovy     |  4 +
 .../document/config/DocumentMappingContext.java    |  2 +-
 .../mapping/config/KeyValueMappingContext.java     |  6 +-
 .../mapping/model/AbstractMappingContext.java      |  6 +-
 .../mapping/model/AbstractPersistentEntity.java    | 10 ++-
 .../datastore/mapping/model/MappingContext.java    | 14 ++++
 .../org/grails/datastore/mapping/query/Query.java  |  2 +-
 .../datastore/mapping/reflect/AstUtils.groovy      |  6 +-
 .../datastore/mapping/reflect/ClassUtils.java      | 27 ++++++
 .../datastore/mapping/services/Service.groovy      | 17 ++--
 .../CustomizableRollbackTransactionAttribute.java  | 43 ++++++----
 .../mapping/core/AbstractDatastoreSpec.groovy      | 96 ++++++++++++++++++++++
 .../core/SessionResolverIntegrationSpec.groovy     | 58 +++++++++++++
 .../core/ThreadLocalSessionResolverSpec.groovy     | 65 +++++++++++++++
 .../services/DefaultServiceRegistrySpec.groovy     |  4 +-
 22 files changed, 563 insertions(+), 63 deletions(-)

diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/AbstractDatastore.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/AbstractDatastore.java
index 7ee641a2bf..630126a214 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/AbstractDatastore.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/AbstractDatastore.java
@@ -14,6 +14,9 @@
  */
 package org.grails.datastore.mapping.core;
 
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import groovy.lang.Closure;
@@ -26,8 +29,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
 import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationListener;
 import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.PayloadApplicationEvent;
 import org.springframework.core.convert.converter.ConverterRegistry;
 import org.springframework.core.env.PropertyResolver;
 import 
org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -55,12 +61,48 @@ import 
org.grails.datastore.mapping.transactions.SessionHolder;
 @SuppressWarnings({"rawtypes", "unchecked"})
 public abstract class AbstractDatastore implements Datastore, 
StatelessDatastore, ServiceRegistry {
     protected static final Logger LOG = 
LoggerFactory.getLogger(AbstractDatastore.class);
+
+    private static final class DefaultApplicationEventPublisher implements 
ApplicationEventPublisher {
+        private final List<ApplicationListener> listeners = new ArrayList<>();
+
+        @Override
+        public void publishEvent(ApplicationEvent event) {
+            publishEvent((Object) event);
+        }
+
+        @Override
+        public void publishEvent(Object event) {
+            for (ApplicationListener listener : new ArrayList<>(listeners)) {
+                if (event instanceof ApplicationEvent) {
+                    listener.onApplicationEvent((ApplicationEvent) event);
+                } else {
+                    listener.onApplicationEvent(new 
PayloadApplicationEvent(this, event));
+                }
+            }
+        }
+
+        public void addApplicationListener(ApplicationListener<?> listener) {
+            listeners.add(listener);
+        }
+    }
+
     private ApplicationContext applicationContext;
+    protected ApplicationEventPublisher applicationEventPublisher = new 
DefaultApplicationEventPublisher();
 
     protected final MappingContext mappingContext;
     protected final ServiceRegistry serviceRegistry;
     protected final PropertyResolver connectionDetails;
     protected final TPCacheAdapterRepository cacheAdapterRepository;
+    protected SessionResolver sessionResolver;
+
+    @Override
+    public SessionResolver getSessionResolver() {
+        return sessionResolver;
+    }
+
+    public void setSessionResolver(SessionResolver sessionResolver) {
+        this.sessionResolver = sessionResolver;
+    }
 
     public AbstractDatastore(MappingContext mappingContext) {
         this(mappingContext, (PropertyResolver) null, null);
@@ -80,8 +122,10 @@ public abstract class AbstractDatastore implements 
Datastore, StatelessDatastore
                              ConfigurableApplicationContext ctx, 
TPCacheAdapterRepository cacheAdapterRepository) {
         this.mappingContext = mappingContext;
         this.connectionDetails = connectionDetails;
-        setApplicationContext(ctx);
         this.cacheAdapterRepository = cacheAdapterRepository;
+        this.applicationEventPublisher = ctx != null ? ctx : new 
DefaultApplicationEventPublisher();
+        this.sessionResolver = new ThreadLocalSessionResolver<>();
+        setApplicationContext(ctx);
         DefaultServiceRegistry defaultServiceRegistry = new 
DefaultServiceRegistry(this);
         this.serviceRegistry = defaultServiceRegistry;
         defaultServiceRegistry.initialize();
@@ -122,6 +166,36 @@ public abstract class AbstractDatastore implements 
Datastore, StatelessDatastore
 
     public void setApplicationContext(ApplicationContext ctx) {
         applicationContext = ctx;
+        if (ctx instanceof ApplicationEventPublisher) {
+            this.applicationEventPublisher = (ApplicationEventPublisher) ctx;
+        }
+        else if (ctx == null && !(this.applicationEventPublisher instanceof 
DefaultApplicationEventPublisher)) {
+            this.applicationEventPublisher = new 
DefaultApplicationEventPublisher();
+        }
+    }
+
+    public void setApplicationEventPublisher(ApplicationEventPublisher 
applicationEventPublisher) {
+        this.applicationEventPublisher = applicationEventPublisher;
+    }
+
+    /**
+     * Adds an application listener to the datastore
+     * @param listener The listener
+     */
+    public void addApplicationListener(ApplicationListener<?> listener) {
+        if (applicationEventPublisher instanceof 
ConfigurableApplicationContext) {
+            ((ConfigurableApplicationContext) 
applicationEventPublisher).addApplicationListener(listener);
+        } else if (applicationEventPublisher instanceof 
DefaultApplicationEventPublisher) {
+            ((DefaultApplicationEventPublisher) 
applicationEventPublisher).addApplicationListener(listener);
+        }
+        else {
+            try {
+                Method method = 
applicationEventPublisher.getClass().getMethod("addApplicationListener", 
ApplicationListener.class);
+                method.invoke(applicationEventPublisher, listener);
+            } catch (Exception e) {
+                // ignore
+            }
+        }
     }
 
     public Session connect() {
@@ -171,7 +245,7 @@ public abstract class AbstractDatastore implements 
Datastore, StatelessDatastore
     }
 
     public boolean hasCurrentSession() {
-        return TransactionSynchronizationManager.hasResource(this);
+        return sessionResolver.resolve() != null || 
TransactionSynchronizationManager.hasResource(this);
     }
 
     /**
@@ -223,7 +297,7 @@ public abstract class AbstractDatastore implements 
Datastore, StatelessDatastore
     }
 
     public ApplicationEventPublisher getApplicationEventPublisher() {
-        return getApplicationContext();
+        return applicationEventPublisher;
     }
 
     protected void initializeConverters(MappingContext mappingContext) {
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/Datastore.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/Datastore.java
index 57206ea2e7..300381acd9 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/Datastore.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/Datastore.java
@@ -39,6 +39,11 @@ import org.grails.datastore.mapping.services.ServiceRegistry;
  */
 public interface Datastore extends ServiceRegistry {
 
+    /**
+     * @return The session resolver for this datastore
+     */
+    SessionResolver getSessionResolver();
+
     /**
      * Connects to the datastore with the default connection details, normally 
provided via the datastore implementations constructor
      *
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/DatastoreUtils.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/DatastoreUtils.java
index 7e7e868325..449422e19f 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/DatastoreUtils.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/DatastoreUtils.java
@@ -116,11 +116,15 @@ public abstract class DatastoreUtils {
 
         Assert.notNull(datastore, "No Datastore specified");
 
+        Session session = datastore.getSessionResolver().resolve();
+        if (session != null) {
+            return session;
+        }
+
         SessionHolder sessionHolder = (SessionHolder) 
TransactionSynchronizationManager.getResource(datastore);
 
         if (sessionHolder != null && !sessionHolder.isEmpty()) {
             // pre-bound Datastore Session
-            Session session;
             if (TransactionSynchronizationManager.isSynchronizationActive() &&
                     sessionHolder.doesNotHoldNonDefaultSession()) {
                 // Spring transaction management is active ->
@@ -150,7 +154,7 @@ public abstract class DatastoreUtils {
         if (logger.isDebugEnabled()) {
             logger.debug("Opening Datastore Session");
         }
-        Session session = datastore.connect();
+        session = datastore.connect();
 
         // Use same Session for further Datastore actions within the 
transaction.
         // Thread object will get removed by synchronization at transaction 
completion.
@@ -361,13 +365,61 @@ public abstract class DatastoreUtils {
         }
     }
 
+    /**
+     * Execute the given callback with a new session, regardless of whether an 
existing session is present
+     * @param datastore The datastore
+     * @param callback The callback
+     * @param <T> The return type
+     * @return The result of the callback
+     */
+    public static <T> T executeWithNewSession(Datastore datastore, 
SessionCallback<T> callback) {
+        Session session = bindNewSession(datastore.connect());
+        try {
+            return callback.doInSession(session);
+        }
+        finally {
+            SessionHolder sessionHolder = (SessionHolder) 
TransactionSynchronizationManager.getResource(datastore);
+            if (sessionHolder != null) {
+                sessionHolder.removeSession(session);
+                if (sessionHolder.isEmpty()) {
+                    
TransactionSynchronizationManager.unbindResource(datastore);
+                }
+            }
+            closeSessionOrRegisterDeferredClose(session, datastore);
+        }
+    }
+
+    /**
+     * Execute the given callback with a new session, regardless of whether an 
existing session is present
+     * @param datastore The datastore
+     * @param callback The callback
+     */
+    public static void executeWithNewSession(Datastore datastore, 
VoidSessionCallback callback) {
+        Session session = bindNewSession(datastore.connect());
+        try {
+            callback.doInSession(session);
+        }
+        finally {
+            SessionHolder sessionHolder = (SessionHolder) 
TransactionSynchronizationManager.getResource(datastore);
+            if (sessionHolder != null) {
+                sessionHolder.removeSession(session);
+                if (sessionHolder.isEmpty()) {
+                    
TransactionSynchronizationManager.unbindResource(datastore);
+                }
+            }
+            closeSessionOrRegisterDeferredClose(session, datastore);
+        }
+    }
+
     /**
      * Bind the session to the thread with a SessionHolder keyed by its 
Datastore.
      * @param session the session
      * @return the session (for method chaining)
      */
     public static Session bindSession(final Session session) {
-        TransactionSynchronizationManager.bindResource(session.getDatastore(), 
new SessionHolder(session));
+        if 
(!TransactionSynchronizationManager.hasResource(session.getDatastore())) {
+            
TransactionSynchronizationManager.bindResource(session.getDatastore(), new 
SessionHolder(session));
+        }
         return session;
     }
 
@@ -377,7 +429,9 @@ public abstract class DatastoreUtils {
      * @return the session (for method chaining)
      */
     public static Session bindSession(final Session session, Object creator) {
-        TransactionSynchronizationManager.bindResource(session.getDatastore(), 
new SessionHolder(session, creator));
+        if 
(!TransactionSynchronizationManager.hasResource(session.getDatastore())) {
+            
TransactionSynchronizationManager.bindResource(session.getDatastore(), new 
SessionHolder(session, creator));
+        }
         return session;
     }
 
@@ -489,7 +543,7 @@ public abstract class DatastoreUtils {
         }
         else {
 
-            Map<String, Object>[] configurations = new Map[1];
+            Map<String, Object>[] configurations = (Map<String, Object>[]) new 
Map[1];
             configurations[0] = configuration;
             return createPropertyResolvers(configurations);
         }
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/SessionResolver.groovy
similarity index 58%
copy from 
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
copy to 
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/SessionResolver.groovy
index 337718115b..52447599eb 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/SessionResolver.groovy
@@ -16,34 +16,28 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.grails.datastore.mapping.services
 
-import groovy.transform.Generated
+package org.grails.datastore.mapping.core;
 
-import org.grails.datastore.mapping.core.Datastore
+import groovy.transform.CompileStatic;
 
 /**
- * Represents a service available exposed by the GORM {@link Datastore}
+ * Resolver for sessions in the current context (thread, tenant, etc)
  *
- * @author Graeme Rocher
- * @since 6.1
- *
- * <T> The domain class type this service operates with
+ * @author borinquenkid
+ * @since 8.0
  */
-trait Service<T> {
+@CompileStatic
+public interface SessionResolver<S extends Session> {
+    /** Resolves the current session based on current context (thread, tenant, 
etc) */
+    S resolve();
 
-    /**
-     * The datastore that this service is related to
-     */
-    private Datastore datastore
+    /** Resolves a session for a specific qualifier/tenant */
+    S resolve(String qualifier);
 
-    @Generated
-    Datastore getDatastore() {
-        return datastore
-    }
+    /** Binds a session to the current context */
+    void bind(S session);
 
-    @Generated
-    void setDatastore(Datastore datastore) {
-        this.datastore = datastore
-    }
+    /** Unbinds the current session */
+    void unbind();
 }
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolver.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolver.groovy
new file mode 100644
index 0000000000..80bb972d36
--- /dev/null
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolver.groovy
@@ -0,0 +1,66 @@
+/*
+ *  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.mapping.core;
+
+import groovy.transform.CompileStatic;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A default thread-bound SessionResolver
+ *
+ * @author borinquenkid
+ * @since 8.0
+ */
+@CompileStatic
+public class ThreadLocalSessionResolver<S extends Session> implements 
SessionResolver<S> {
+
+    private final ThreadLocal<S> currentSession = new ThreadLocal<>();
+    private final Map<String, S> qualifiedSessions = new ConcurrentHashMap<>();
+
+    @Override
+    public S resolve() {
+        return currentSession.get();
+    }
+
+    @Override
+    public S resolve(String qualifier) {
+        return qualifiedSessions.get(qualifier);
+    }
+
+    @Override
+    public void bind(S session) {
+        currentSession.set(session);
+        // Note: In a production scenario, we'd need to link the session's 
datastore qualifier here.
+    }
+
+    public void bind(String qualifier, S session) {
+        qualifiedSessions.put(qualifier, session);
+    }
+
+    @Override
+    public void unbind() {
+        currentSession.remove();
+    }
+    
+    public void unbind(String qualifier) {
+        qualifiedSessions.remove(qualifier);
+    }
+}
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/AbstractConnectionSourceFactory.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/AbstractConnectionSourceFactory.java
index e4ab461300..c56b537120 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/AbstractConnectionSourceFactory.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/AbstractConnectionSourceFactory.java
@@ -88,6 +88,17 @@ public abstract class AbstractConnectionSourceFactory<T, S 
extends ConnectionSou
         S settings = buildRuntimeSettings(name, configuration, 
fallbackSettings);
         return create(name, settings);
     }
+
+    /**
+     * Creates the settings for the given configuration
+     * @param configuration The configuration
+     * @return The settings
+     */
+    public S createSettings(PropertyResolver configuration) {
+        ConnectionSourceSettingsBuilder builder = new 
ConnectionSourceSettingsBuilder(configuration);
+        ConnectionSourceSettings fallbackSettings = builder.build();
+        return (S) buildSettings(ConnectionSource.DEFAULT, configuration, 
fallbackSettings, true);
+    }
     
     public <F extends ConnectionSourceSettings> S buildRuntimeSettings(String 
name, PropertyResolver configuration, F fallbackSettings) {
         return buildSettings(name, configuration, fallbackSettings, false);
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/ConnectionSourceSettingsBuilder.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/ConnectionSourceSettingsBuilder.groovy
index 9eecb9f5d9..e23cacc1ca 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/ConnectionSourceSettingsBuilder.groovy
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/connections/ConnectionSourceSettingsBuilder.groovy
@@ -39,6 +39,10 @@ class ConnectionSourceSettingsBuilder extends 
ConfigurationBuilder<ConnectionSou
         super(propertyResolver, configurationPrefix)
     }
 
+    ConnectionSourceSettingsBuilder(PropertyResolver propertyResolver, String 
configurationPrefix, Object fallBackConfiguration) {
+        super(propertyResolver, configurationPrefix, fallBackConfiguration)
+    }
+
     @Override
     protected ConnectionSourceSettings createBuilder() {
         return new ConnectionSourceSettings()
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSupport.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSupport.groovy
index 7fb541e908..de799c8261 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSupport.groovy
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSupport.groovy
@@ -80,6 +80,10 @@ class DirtyCheckingSupport {
                             if (coll.isDirty()) return true
                         }
                     }
+                    else if (value instanceof DirtyCheckableCollection) {
+                        DirtyCheckableCollection coll = 
(DirtyCheckableCollection) value
+                        if (coll.hasChanged()) return true
+                    }
                 }
 
             }
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/document/config/DocumentMappingContext.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/document/config/DocumentMappingContext.java
index abfc980892..70b6210bda 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/document/config/DocumentMappingContext.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/document/config/DocumentMappingContext.java
@@ -69,7 +69,7 @@ public class DocumentMappingContext extends 
AbstractMappingContext {
     }
 
     @Override
-    protected void initialize(ConnectionSourceSettings settings) {
+    public void initialize(ConnectionSourceSettings settings) {
 
         this.defaultMapping = settings.getDefault().getMapping();
         AbstractGormMappingFactory documentMappingFactory = 
(AbstractGormMappingFactory) createDocumentMappingFactory(defaultMapping);
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/keyvalue/mapping/config/KeyValueMappingContext.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/keyvalue/mapping/config/KeyValueMappingContext.java
index 15842f2a86..6b4983e8f8 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/keyvalue/mapping/config/KeyValueMappingContext.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/keyvalue/mapping/config/KeyValueMappingContext.java
@@ -25,7 +25,7 @@ import 
org.grails.datastore.mapping.model.AbstractMappingContext;
 import org.grails.datastore.mapping.model.MappingConfigurationStrategy;
 import org.grails.datastore.mapping.model.MappingFactory;
 import org.grails.datastore.mapping.model.PersistentEntity;
-import 
org.grails.datastore.mapping.model.config.JpaMappingConfigurationStrategy;
+import 
org.grails.datastore.mapping.model.config.GormMappingConfigurationStrategy;
 
 /**
  * A MappingContext used to map objects to a Key/Value store
@@ -54,7 +54,7 @@ public class KeyValueMappingContext extends 
AbstractMappingContext {
         Assert.notNull(keyspace, "Argument [keyspace] cannot be null");
         this.keyspace = keyspace;
         initializeDefaultMappingFactory(keyspace);
-        syntaxStrategy = new JpaMappingConfigurationStrategy(mappingFactory);
+        syntaxStrategy = new GormMappingConfigurationStrategy(mappingFactory);
         super.initialize(new ConnectionSourceSettings());
     }
 
@@ -67,7 +67,7 @@ public class KeyValueMappingContext extends 
AbstractMappingContext {
         Assert.notNull(keyspace, "Argument [keyspace] cannot be null");
         this.keyspace = keyspace;
         initializeDefaultMappingFactory(keyspace);
-        syntaxStrategy = new 
JpaMappingConfigurationStrategy(this.mappingFactory);
+        syntaxStrategy = new 
GormMappingConfigurationStrategy(this.mappingFactory);
         super.initialize(settings);
     }
 
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractMappingContext.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractMappingContext.java
index 8afab3ed45..6415cdf414 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractMappingContext.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractMappingContext.java
@@ -91,7 +91,11 @@ public abstract class AbstractMappingContext implements 
MappingContext, Initiali
         return this.multiTenancyMode;
     }
 
-    protected void initialize(ConnectionSourceSettings settings) {
+    public void setMultiTenancyMode(MultiTenancySettings.MultiTenancyMode 
multiTenancyMode) {
+        this.multiTenancyMode = multiTenancyMode;
+    }
+
+    public void initialize(ConnectionSourceSettings settings) {
         FieldEntityAccess.clearReflectors();
         this.multiTenancyMode = settings.getMultiTenancy().getMode();
 
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractPersistentEntity.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractPersistentEntity.java
index f4f56dc091..ecab43c54b 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractPersistentEntity.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AbstractPersistentEntity.java
@@ -97,6 +97,14 @@ public abstract class AbstractPersistentEntity<T extends 
Entity> implements Pers
     }
 
     public TenantId getTenantId() {
+        if (this.tenantId == null && isMultiTenant()) {
+            for (PersistentProperty prop : persistentProperties) {
+                if (prop instanceof TenantId) {
+                    this.tenantId = (TenantId) prop;
+                    break;
+                }
+            }
+        }
         return tenantId;
     }
 
@@ -152,7 +160,7 @@ public abstract class AbstractPersistentEntity<T extends 
Entity> implements Pers
             associations = new ArrayList();
             embedded = new ArrayList();
 
-            boolean multiTenancyEnabled = isMultiTenant && 
context.getMultiTenancyMode() == 
MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR;
+            boolean multiTenancyEnabled = isMultiTenant() && 
context.getMultiTenancyMode() == 
MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR;
             for (PersistentProperty persistentProperty : persistentProperties) 
{
                 if (multiTenancyEnabled && persistentProperty instanceof 
TenantId) {
                     this.tenantId = (TenantId) persistentProperty;
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/MappingContext.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/MappingContext.java
index 9300985ec7..ee4b5d0a20 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/MappingContext.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/MappingContext.java
@@ -27,6 +27,7 @@ import org.springframework.validation.Validator;
 
 import org.grails.datastore.mapping.engine.EntityAccess;
 import org.grails.datastore.mapping.multitenancy.MultiTenancySettings;
+import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings;
 import org.grails.datastore.mapping.proxy.ProxyFactory;
 import org.grails.datastore.mapping.proxy.ProxyHandler;
 import org.grails.datastore.mapping.reflect.EntityReflector;
@@ -51,11 +52,24 @@ import 
org.grails.datastore.mapping.validation.ValidatorRegistry;
 @SuppressWarnings("rawtypes")
 public interface MappingContext {
 
+    /**
+     * Initialize the mapping context with the given settings
+     * @param settings The settings
+     */
+    void initialize(ConnectionSourceSettings settings);
+
     /**
      * @return The multi tenancy mode
      */
     MultiTenancySettings.MultiTenancyMode getMultiTenancyMode();
 
+    /**
+     * Set the multi tenancy mode
+     *
+     * @param multiTenancyMode The multi tenancy mode
+     */
+    void setMultiTenancyMode(MultiTenancySettings.MultiTenancyMode 
multiTenancyMode);
+
     /**
      * Obtains a list of PersistentEntity instances
      *
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/query/Query.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/query/Query.java
index faaf1cbace..d7c9207e54 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/query/Query.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/query/Query.java
@@ -647,7 +647,7 @@ public abstract class Query implements Cloneable, 
Serializable {
 
         ApplicationEventPublisher publisher = 
session.getDatastore().getApplicationEventPublisher();
         if (publisher != null) {
-            publisher.publishEvent(new PreQueryEvent(this));
+            publisher.publishEvent(new PreQueryEvent(session.getDatastore(), 
this));
         }
 
         List results = executeQuery(entity, criteria);
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy
index edcb67296e..dba5a9f744 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy
@@ -358,8 +358,10 @@ class AstUtils {
             String annotationClassName = node.getClassNode().getName()
             if ((excluded == null || !excluded.contains(annotationClassName)) 
&&
                     (included == null || 
included.contains(annotationClassName))) {
-                final AnnotationNode copyOfAnnotationNode = 
cloneAnnotation(node)
-                to.addAnnotation(copyOfAnnotationNode)
+                if (to.getAnnotations(node.getClassNode()).isEmpty()) {
+                    final AnnotationNode copyOfAnnotationNode = 
cloneAnnotation(node)
+                    to.addAnnotation(copyOfAnnotationNode)
+                }
             }
         }
     }
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/ClassUtils.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/ClassUtils.java
index c55fe8d5b7..1f4b77095c 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/ClassUtils.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/ClassUtils.java
@@ -119,6 +119,33 @@ public class ClassUtils {
         }
     }
 
+    /**
+     * Retrieves an integer value from a Map for the given key.
+     *
+     * @param key The key that references the integer value
+     * @param map The map to look in
+     * @return An Integer value, or {@code null} if the map is null, does not 
contain the key, or the value is null
+     */
+    public static Integer getIntegerFromMap(String key, Map<?, ?> map) {
+        if (map == null) return null;
+        if (map.containsKey(key)) {
+            Object o = map.get(key);
+            if (o == null) return null;
+            if (o instanceof Integer) {
+                return (Integer) o;
+            }
+            if (o instanceof Number) {
+                return ((Number) o).intValue();
+            }
+            try {
+                return Integer.valueOf(o.toString());
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
     /**
      * Retrieves a boolean value from a Map for the given key
      *
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
index 337718115b..1561b90a6b 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/services/Service.groovy
@@ -33,17 +33,16 @@ import org.grails.datastore.mapping.core.Datastore
 trait Service<T> {
 
     /**
-     * The datastore that this service is related to
+     * @return The datastore that this service is related to
      */
-    private Datastore datastore
-
     @Generated
-    Datastore getDatastore() {
-        return datastore
-    }
+    abstract Datastore getDatastore()
 
+    /**
+     * Sets the datastore
+     * @param datastore The datastore
+     */
     @Generated
-    void setDatastore(Datastore datastore) {
-        this.datastore = datastore
-    }
+    abstract void setDatastore(Datastore datastore)
+
 }
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/transactions/CustomizableRollbackTransactionAttribute.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/transactions/CustomizableRollbackTransactionAttribute.java
index 3f55de0a66..f78a8678bb 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/transactions/CustomizableRollbackTransactionAttribute.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/transactions/CustomizableRollbackTransactionAttribute.java
@@ -28,6 +28,7 @@ import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
 import org.springframework.transaction.interceptor.RollbackRuleAttribute;
 import 
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
+import org.springframework.transaction.interceptor.TransactionAttribute;
 
 /**
  * Extended version of {@link RuleBasedTransactionAttribute} that ensures all 
exception types are rolled back and allows inheritance of setRollbackOnly
@@ -51,38 +52,48 @@ public class CustomizableRollbackTransactionAttribute 
extends RuleBasedTransacti
         super(propagationBehavior, rollbackRules);
     }
 
-    public 
CustomizableRollbackTransactionAttribute(org.springframework.transaction.interceptor.TransactionAttribute
 other) {
+    public CustomizableRollbackTransactionAttribute(TransactionAttribute 
other) {
         super();
-        setPropagationBehavior(other.getPropagationBehavior());
-        setIsolationLevel(other.getIsolationLevel());
-        setTimeout(other.getTimeout());
-        setReadOnly(other.isReadOnly());
-        setName(other.getName());
+        copyFrom(other);
     }
 
     public CustomizableRollbackTransactionAttribute(TransactionDefinition 
other) {
         super();
-        setPropagationBehavior(other.getPropagationBehavior());
-        setIsolationLevel(other.getIsolationLevel());
-        setTimeout(other.getTimeout());
-        setReadOnly(other.isReadOnly());
-        setName(other.getName());
+        copyFrom(other);
     }
 
     public 
CustomizableRollbackTransactionAttribute(CustomizableRollbackTransactionAttribute
 other) {
-        this((RuleBasedTransactionAttribute) other);
+        super();
+        copyFrom(other);
     }
 
     public 
CustomizableRollbackTransactionAttribute(RuleBasedTransactionAttribute other) {
+        super();
+        copyFrom(other);
+    }
+
+    protected void copyFrom(TransactionDefinition other) {
+        setPropagationBehavior(other.getPropagationBehavior());
+        setIsolationLevel(other.getIsolationLevel());
+        setTimeout(other.getTimeout());
+        setReadOnly(other.isReadOnly());
+        setName(other.getName());
+        if (other instanceof TransactionAttribute) {
+            setQualifier(((TransactionAttribute) other).getQualifier());
+        }
+        if (other instanceof RuleBasedTransactionAttribute) {
+            setRollbackRules(((RuleBasedTransactionAttribute) 
other).getRollbackRules());
+        }
         if (other instanceof CustomizableRollbackTransactionAttribute) {
             this.inheritRollbackOnly = 
((CustomizableRollbackTransactionAttribute) other).inheritRollbackOnly;
+            this.connection = ((CustomizableRollbackTransactionAttribute) 
other).connection;
         }
     }
 
     @Override
     public boolean rollbackOn(Throwable ex) {
         if (log.isTraceEnabled()) {
-            log.trace("Applying rules to determine whether transaction should 
rollback on $ex");
+            log.trace("Applying rules to determine whether transaction should 
rollback on " + ex);
         }
 
         RollbackRuleAttribute winner = null;
@@ -100,12 +111,14 @@ public class CustomizableRollbackTransactionAttribute 
extends RuleBasedTransacti
         }
 
         if (log.isTraceEnabled()) {
-            log.trace("Winning rollback rule is: $winner");
+            log.trace("Winning rollback rule is: " + winner);
         }
 
         // User superclass behavior (rollback on unchecked) if no rule matches.
         if (winner == null) {
-            log.trace("No relevant rollback rule found: applying default 
rules");
+            if (log.isTraceEnabled()) {
+                log.trace("No relevant rollback rule found: applying default 
rules");
+            }
 
             // always rollback regardless if it is a checked or unchecked 
exception since Groovy doesn't differentiate those
             return true;
diff --git 
a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/AbstractDatastoreSpec.groovy
 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/AbstractDatastoreSpec.groovy
new file mode 100644
index 0000000000..cbe753482a
--- /dev/null
+++ 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/AbstractDatastoreSpec.groovy
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.mapping.core
+
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.springframework.context.ApplicationEventPublisher
+import org.springframework.context.ConfigurableApplicationContext
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.core.env.PropertyResolver
+import spock.lang.Specification
+
+class AbstractDatastoreSpec extends Specification {
+
+    void "test that getApplicationEventPublisher returns the application 
context if set"() {
+        given:
+        def mappingContext = Mock(MappingContext)
+        def ctx = new GenericApplicationContext()
+        ctx.refresh()
+        def datastore = new TestDatastore(mappingContext, 
(PropertyResolver)null, ctx)
+
+        expect:
+        datastore.applicationEventPublisher == ctx
+        datastore.applicationContext == ctx
+    }
+
+    void "test that SessionCreationEvent is published when connect is 
called"() {
+        given:
+        def mappingContext = Mock(MappingContext)
+        def events = []
+        def publisher = [
+            publishEvent: { event -> events << event }
+        ] as ApplicationEventPublisher
+        
+        // Note: GenericApplicationContext implements ApplicationEventPublisher
+        def ctx = new GenericApplicationContext()
+        ctx.addApplicationListener({ event -> events << event })
+        ctx.refresh()
+        
+        def datastore = new TestDatastore(mappingContext, 
(PropertyResolver)null, ctx)
+        def mockSession = Mock(Session)
+        mockSession.getDatastore() >> datastore
+        datastore.sessionCreator = { mockSession }
+
+        when:
+        def session = datastore.connect()
+
+        then:
+        session == mockSession
+        events.any { it instanceof SessionCreationEvent }
+        ((SessionCreationEvent)events.find { it instanceof 
SessionCreationEvent }).session == session
+    }
+
+    void "test that getApplicationEventPublisher returns the standalone 
publisher if set"() {
+        given:
+        def mappingContext = Mock(MappingContext)
+        def events = []
+        def publisher = [
+            publishEvent: { event -> events << event }
+        ] as ApplicationEventPublisher
+        
+        def datastore = new TestDatastore(mappingContext, 
(PropertyResolver)null, null)
+        datastore.setApplicationEventPublisher(publisher)
+        
+        expect:
+        datastore.getApplicationEventPublisher() == publisher
+        datastore.applicationContext == null
+    }
+
+    static class TestDatastore extends AbstractDatastore {
+        Closure<Session> sessionCreator = { null }
+        
+        TestDatastore(MappingContext mappingContext, PropertyResolver 
connectionDetails, ConfigurableApplicationContext ctx) {
+            super(mappingContext, (PropertyResolver)connectionDetails, ctx)
+        }
+
+        @Override
+        protected Session createSession(PropertyResolver connectionDetails) {
+            return sessionCreator.call(connectionDetails)
+        }
+    }
+}
diff --git 
a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/SessionResolverIntegrationSpec.groovy
 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/SessionResolverIntegrationSpec.groovy
new file mode 100644
index 0000000000..bc41e9f1e1
--- /dev/null
+++ 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/SessionResolverIntegrationSpec.groovy
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.mapping.core
+
+import org.grails.datastore.mapping.model.MappingContext
+import org.springframework.core.env.PropertyResolver
+import spock.lang.Specification
+
+class SessionResolverIntegrationSpec extends Specification {
+
+    void "test session resolution through datastore"() {
+        given:
+        def datastore = new TestDatastore(Mock(MappingContext))
+        def session = Mock(Session)
+        
+        // Ensure resolver is available
+        def resolver = datastore.getSessionResolver()
+        
+        when:
+        resolver.bind(session)
+        
+        then:
+        resolver.resolve() == session
+        
+        when:
+        resolver.unbind()
+        
+        then:
+        resolver.resolve() == null
+    }
+
+    static class TestDatastore extends AbstractDatastore {
+        TestDatastore(MappingContext mappingContext) {
+            super(mappingContext)
+            // Manually inject the resolver since we are testing the 
integration
+            this.sessionResolver = new ThreadLocalSessionResolver<Session>()
+        }
+
+        @Override
+        protected Session createSession(PropertyResolver connectionDetails) {
+            return null
+        }
+    }
+}
diff --git 
a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolverSpec.groovy
 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolverSpec.groovy
new file mode 100644
index 0000000000..b879e8746e
--- /dev/null
+++ 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/ThreadLocalSessionResolverSpec.groovy
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.mapping.core
+
+import spock.lang.Specification
+
+class ThreadLocalSessionResolverSpec extends Specification {
+
+    ThreadLocalSessionResolver<Session> resolver = new 
ThreadLocalSessionResolver<>()
+
+    def "should bind and resolve session"() {
+        given:
+        Session session = Mock(Session)
+
+        when:
+        resolver.bind(session)
+
+        then:
+        resolver.resolve() == session
+
+        cleanup:
+        resolver.unbind()
+    }
+
+    def "should bind and resolve qualified session"() {
+        given:
+        Session session = Mock(Session)
+        String qualifier = "secondary"
+
+        when:
+        resolver.bind(qualifier, session)
+
+        then:
+        resolver.resolve(qualifier) == session
+
+        cleanup:
+        resolver.unbind(qualifier)
+    }
+
+    def "should unbind session"() {
+        given:
+        Session session = Mock(Session)
+        resolver.bind(session)
+
+        when:
+        resolver.unbind()
+
+        then:
+        resolver.resolve() == null
+    }
+}
diff --git 
a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/services/DefaultServiceRegistrySpec.groovy
 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/services/DefaultServiceRegistrySpec.groovy
index 16f6864bc3..4bc63ff5de 100644
--- 
a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/services/DefaultServiceRegistrySpec.groovy
+++ 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/services/DefaultServiceRegistrySpec.groovy
@@ -53,6 +53,8 @@ class DefaultServiceRegistrySpec extends Specification {
     }
 }
 
-class TestService implements Service, ITestService {}
+class TestService implements Service, ITestService {
+    Datastore datastore
+}
 
 interface ITestService {}


Reply via email to