You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/04/15 20:48:32 UTC

(grails-core) 03/24: Refactor nested datastore classes and fix lifecycle issues in multitenancy

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

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

commit e6f6e2462ef3817c5107394dc28dee3d826b4d76
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Apr 12 09:36:18 2026 -0500

    Refactor nested datastore classes and fix lifecycle issues in multitenancy
---
 .../orm/hibernate/ChildHibernateDatastore.java     |  74 +++++++++++++
 .../grails/orm/hibernate/HibernateDatastore.java   | 119 +++------------------
 .../orm/hibernate/SchemaTenantGormEnhancer.java    |  94 ++++++++++++++++
 .../ChildHibernateDatastoreUnitSpec.groovy         |   2 +-
 4 files changed, 181 insertions(+), 108 deletions(-)

diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java
new file mode 100644
index 0000000000..f526518320
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate;
+
+import org.grails.datastore.mapping.core.connections.ConnectionSource;
+import org.grails.datastore.mapping.core.connections.ConnectionSources;
+import org.grails.orm.hibernate.cfg.HibernateMappingContext;
+import org.grails.orm.hibernate.cfg.Settings;
+import org.hibernate.SessionFactory;
+import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher;
+import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings;
+
+/**
+ * A datastore for a specific connection in a multiple data source setup.
+ */
+public class ChildHibernateDatastore extends HibernateDatastore {
+
+    private final HibernateDatastore parent;
+
+    public ChildHibernateDatastore(
+            HibernateDatastore parent,
+            ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources,
+            HibernateMappingContext mappingContext,
+            ConfigurableApplicationEventPublisher eventPublisher) {
+        super(connectionSources, mappingContext, eventPublisher,
+            connectionSources.getDefaultConnectionSource().getSource());
+        this.parent = parent;
+    }
+
+    @Override
+    protected HibernateGormEnhancer initialize() {
+        return null;
+    }
+
+    @Override
+    public void destroy() {
+        if (!this.destroyed) {
+            // Only mark as destroyed, don't close shared resources
+            this.destroyed = true;
+        }
+    }
+
+    @Override
+    public HibernateDatastore getDatastoreForConnection(String connectionName) {
+        if (Settings.SETTING_DATASOURCE.equals(connectionName) ||
+                ConnectionSource.DEFAULT.equals(connectionName)) {
+            return parent;
+        } else {
+            HibernateDatastore hibernateDatastore = parent.datastoresByConnectionSource.get(connectionName);
+            if (hibernateDatastore == null) {
+                throw new org.grails.datastore.mapping.core.exceptions.ConfigurationException(
+                        "DataSource not found for name [" + connectionName +
+                                "] in configuration. Please check your multiple data sources configuration and try again.");
+            }
+            return hibernateDatastore;
+        }
+    }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
index 5326bf8b14..b99cdd5dd5 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
@@ -158,9 +158,8 @@ public class HibernateDatastore extends AbstractDatastore
             HibernateQueryArgument.CONFIG_PASS_READONLY.value();
 
     /** The session factory. */
-    private static final String INFORMATION_SCHEMA = "INFORMATION_SCHEMA";
-
-    private static final String PUBLIC_SCHEMA = "PUBLIC";
+    public static final String INFORMATION_SCHEMA = "INFORMATION_SCHEMA";
+    public static final String PUBLIC_SCHEMA = "PUBLIC";
 
     protected SessionFactory sessionFactory;
 
@@ -203,7 +202,7 @@ public class HibernateDatastore extends AbstractDatastore
     /** The tenant resolver. */
     protected final TenantResolver tenantResolver;
 
-    private boolean destroyed;
+    protected boolean destroyed;
 
     protected final GrailsHibernateTransactionManager transactionManager;
     protected final ConfigurableApplicationEventPublisher eventPublisher;
@@ -226,10 +225,10 @@ public class HibernateDatastore extends AbstractDatastore
         this(connectionSources, mappingContext, eventPublisher, null);
     }
 
-    private HibernateDatastore(
-            final ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources,
-            final HibernateMappingContext mappingContext,
-            final ConfigurableApplicationEventPublisher eventPublisher,
+    protected HibernateDatastore(
+            ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources,
+            HibernateMappingContext mappingContext,
+            ConfigurableApplicationEventPublisher eventPublisher,
             SessionFactory sessionFactory) {
         super(mappingContext, connectionSources.getBaseConfiguration(), null);
         this.connectionSources = connectionSources;
@@ -639,6 +638,11 @@ public class HibernateDatastore extends AbstractDatastore
     public void destroy() {
         if (!this.destroyed) {
             try {
+                for (HibernateDatastore childDatastore : datastoresByConnectionSource.values()) {
+                    if (childDatastore != this && childDatastore.getMappingContext() != getMappingContext()) {
+                        childDatastore.destroy();
+                    }
+                }
                 super.destroy();
                 HibernateGormInstanceApi.resetInsertActive();
                 try {
@@ -994,103 +998,4 @@ public class HibernateDatastore extends AbstractDatastore
             }
         }
     }
-
-    /**
-     * A {@link HibernateGormEnhancer} for SCHEMA multi-tenancy mode that resolves all tenant qualifiers
-     * from either the registered {@link AllTenantsResolver} or the available schema names on the data source.
-     */
-    public static class SchemaTenantGormEnhancer extends HibernateGormEnhancer {
-
-        private final HibernateConnectionSource defaultConnectionSource;
-        private final TenantResolver tenantResolver;
-        private final SchemaHandler schemaHandler;
-        private final Map<String, HibernateDatastore> datastoresByConnectionSource;
-
-        public SchemaTenantGormEnhancer(
-                Datastore datastore,
-                PlatformTransactionManager transactionManager,
-                HibernateConnectionSource defaultConnectionSource,
-                TenantResolver tenantResolver,
-                SchemaHandler schemaHandler,
-                Map<String, HibernateDatastore> datastoresByConnectionSource) {
-            super(datastore, transactionManager, defaultConnectionSource.getSettings());
-            this.defaultConnectionSource = defaultConnectionSource;
-            this.tenantResolver = tenantResolver;
-            this.schemaHandler = schemaHandler;
-            this.datastoresByConnectionSource = datastoresByConnectionSource;
-            // super() calls registerEntity → allQualifiers before our fields are set.
-            // Re-register now that all fields are initialized so schema qualifiers are wired correctly.
-            for (PersistentEntity entity : datastore.getMappingContext().getPersistentEntities()) {
-                registerEntity(entity);
-            }
-        }
-
-        @Override
-        public List<String> allQualifiers(Datastore datastore, PersistentEntity entity) {
-            List<String> allQualifiers = super.allQualifiers(datastore, entity);
-            // Guard against being called from super() before our fields are initialized.
-            if (defaultConnectionSource == null) {
-                return allQualifiers;
-            }
-            if (MultiTenant.class.isAssignableFrom(entity.getJavaClass())) {
-                if (tenantResolver instanceof AllTenantsResolver allTenantsResolver) {
-                    Iterable<Serializable> tenantIds = allTenantsResolver.resolveTenantIds();
-                    for (Serializable id : tenantIds) {
-                        allQualifiers.add(id.toString());
-                    }
-                } else {
-                    Collection<String> schemaNames =
-                            schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource());
-                    for (String schemaName : schemaNames) {
-                        if (INFORMATION_SCHEMA.equals(schemaName) || PUBLIC_SCHEMA.equals(schemaName)) continue;
-                        for (String connectionName : datastoresByConnectionSource.keySet()) {
-                            if (schemaName.equalsIgnoreCase(connectionName)) {
-                                allQualifiers.add(connectionName);
-                            }
-                        }
-                    }
-                }
-            }
-            return allQualifiers;
-        }
-    }
-
-    /**
-     * A datastore for a specific connection in a multiple data source setup.
-     */
-    public static class ChildHibernateDatastore extends HibernateDatastore {
-
-        private final HibernateDatastore parent;
-
-        public ChildHibernateDatastore(
-                HibernateDatastore parent,
-                ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources,
-                HibernateMappingContext mappingContext,
-                ConfigurableApplicationEventPublisher eventPublisher) {
-            super(connectionSources, mappingContext, eventPublisher,
-                connectionSources.getDefaultConnectionSource().getSource());
-            this.parent = parent;
-        }
-
-        @Override
-        protected HibernateGormEnhancer initialize() {
-            return null;
-        }
-
-        @Override
-        public HibernateDatastore getDatastoreForConnection(String connectionName) {
-            if (Settings.SETTING_DATASOURCE.equals(connectionName) ||
-                    ConnectionSource.DEFAULT.equals(connectionName)) {
-                return parent;
-            } else {
-                HibernateDatastore hibernateDatastore = parent.datastoresByConnectionSource.get(connectionName);
-                if (hibernateDatastore == null) {
-                    throw new ConfigurationException(
-                            "DataSource not found for name [" + connectionName +
-                                    "] in configuration. Please check your multiple data sources configuration and try again.");
-                }
-                return hibernateDatastore;
-            }
-        }
-    }
 }
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java
new file mode 100644
index 0000000000..be412f3c99
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate;
+
+import grails.gorm.MultiTenant;
+import org.grails.datastore.mapping.core.Datastore;
+import org.grails.datastore.mapping.multitenancy.AllTenantsResolver;
+import org.grails.datastore.mapping.multitenancy.TenantResolver;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.gorm.jdbc.schema.SchemaHandler;
+import org.grails.orm.hibernate.connections.HibernateConnectionSource;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link HibernateGormEnhancer} for SCHEMA multi-tenancy mode that resolves all tenant qualifiers
+ * from either the registered {@link AllTenantsResolver} or the available schema names on the data source.
+ */
+public class SchemaTenantGormEnhancer extends HibernateGormEnhancer {
+
+    private final HibernateConnectionSource defaultConnectionSource;
+    private final TenantResolver tenantResolver;
+    private final SchemaHandler schemaHandler;
+    private final Map<String, HibernateDatastore> datastoresByConnectionSource;
+
+    public SchemaTenantGormEnhancer(
+            Datastore datastore,
+            PlatformTransactionManager transactionManager,
+            HibernateConnectionSource defaultConnectionSource,
+            TenantResolver tenantResolver,
+            SchemaHandler schemaHandler,
+            Map<String, HibernateDatastore> datastoresByConnectionSource) {
+        super(datastore, transactionManager, defaultConnectionSource.getSettings());
+        this.defaultConnectionSource = defaultConnectionSource;
+        this.tenantResolver = tenantResolver;
+        this.schemaHandler = schemaHandler;
+        this.datastoresByConnectionSource = datastoresByConnectionSource;
+        // super() calls registerEntity → allQualifiers before our fields are set.
+        // Re-register now that all fields are initialized so schema qualifiers are wired correctly.
+        for (PersistentEntity entity : datastore.getMappingContext().getPersistentEntities()) {
+            registerEntity(entity);
+        }
+    }
+
+    @Override
+    public List<String> allQualifiers(Datastore datastore, PersistentEntity entity) {
+        List<String> allQualifiers = super.allQualifiers(datastore, entity);
+        // Guard against being called from super() before our fields are initialized.
+        if (defaultConnectionSource == null) {
+            return allQualifiers;
+        }
+        if (MultiTenant.class.isAssignableFrom(entity.getJavaClass())) {
+            if (tenantResolver instanceof AllTenantsResolver allTenantsResolver) {
+                Iterable<Serializable> tenantIds = allTenantsResolver.resolveTenantIds();
+                for (Serializable id : tenantIds) {
+                    allQualifiers.add(id.toString());
+                }
+            } else {
+                Collection<String> schemaNames =
+                        schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource());
+                for (String schemaName : schemaNames) {
+                    if (HibernateDatastore.INFORMATION_SCHEMA.equals(schemaName) || HibernateDatastore.PUBLIC_SCHEMA.equals(schemaName)) continue;
+                    for (String connectionName : datastoresByConnectionSource.keySet()) {
+                        if (schemaName.equalsIgnoreCase(connectionName)) {
+                            allQualifiers.add(connectionName);
+                        }
+                    }
+                }
+            }
+        }
+        return allQualifiers;
+    }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
index e7719b6ec3..bf1f4a57fb 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
@@ -48,7 +48,7 @@ class ChildHibernateDatastoreUnitSpec extends HibernateGormDatastoreSpec {
         def secondaryConnectionSource = factory.create("secondary", dataSourceConnectionSource, settings)
         
         when: "A child datastore is created"
-        def child = new HibernateDatastore.ChildHibernateDatastore(
+        def child = new ChildHibernateDatastore(
                 parent, 
                 new SingletonConnectionSources(secondaryConnectionSource, parent.connectionSources.getBaseConfiguration()),
                 parent.mappingContext,