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

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

commit 1c4d710990901bba1566a5887b0cccbc478e9539
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Feb 15 11:56:08 2026 -0600

    Refactor single table subclass binding to a dedicated binder class
    
    - Create SingleTableSubclassBinder to handle table-per-hierarchy mapping.
    - Update GrailsDomainBinder to use SingleTableSubclassBinder as a local 
dependency.
    - Add SingleTableSubclassBinderSpec using real entity classes for 
comprehensive testing.
---
 .../orm/hibernate/cfg/GrailsDomainBinder.java      | 31 ++++-------
 .../binder/SingleTableSubclassBinder.java          | 40 ++++++++++++++
 .../binder/SingleTableSubclassBinderSpec.groovy    | 61 ++++++++++++++++++++++
 3 files changed, 112 insertions(+), 20 deletions(-)

diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
index 677c20ade4..e000e4c45d 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
@@ -40,6 +40,7 @@ import 
org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleIdBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.binder.NaturalIdentifierBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder;
+import 
org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubclassBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.binder.UnionSubclassBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.binder.VersionBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder;
@@ -232,12 +233,13 @@ public class GrailsDomainBinder
         MultiTenantFilterBinder multiTenantFilterBinder = new 
MultiTenantFilterBinder();
         JoinedSubClassBinder joinedSubClassBinder = new 
JoinedSubClassBinder(metadataBuildingContext, namingStrategy, new 
SimpleValueColumnBinder(), columnNameForPropertyAndPathFetcher, classBinder);
         UnionSubclassBinder unionSubclassBinder = new 
UnionSubclassBinder(metadataBuildingContext, namingStrategy, classBinder);
+        SingleTableSubclassBinder singleTableSubclassBinder = new 
SingleTableSubclassBinder(classBinder);
 
         hibernateMappingContext
                 .getHibernatePersistentEntities(dataSourceName)
                 .stream()
                 .filter(persistentEntity -> 
persistentEntity.forGrailsDomainMapping(dataSourceName))
-                .forEach(hibernatePersistentEntity -> 
bindRoot(hibernatePersistentEntity, metadataCollector, sessionFactoryName, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, identityBinder, 
versionBinder, grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder));
+                .forEach(hibernatePersistentEntity -> 
bindRoot(hibernatePersistentEntity, metadataCollector, sessionFactoryName, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, identityBinder, 
versionBinder, grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder, 
singleTableSubclassBinder));
     }
 
 
@@ -268,7 +270,7 @@ public class GrailsDomainBinder
      * @param mappings    The Hibernate Mappings object
      * @param sessionFactoryBeanName  the session factory bean name
      */
-    protected void bindRoot(@Nonnull GrailsHibernatePersistentEntity 
entity,@Nonnull InFlightMetadataCollector mappings, String 
sessionFactoryBeanName, DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
IdentityBinder identityBinder, VersionBinder versionBinder, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenantFi [...]
+    protected void bindRoot(@Nonnull GrailsHibernatePersistentEntity 
entity,@Nonnull InFlightMetadataCollector mappings, String 
sessionFactoryBeanName, DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
IdentityBinder identityBinder, VersionBinder versionBinder, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenantFi [...]
         if (mappings.getEntityBinding(entity.getName()) != null) {
             LOG.info("[GrailsDomainBinder] Class [" + entity.getName() + "] is 
already mapped, skipping.. ");
             return;
@@ -285,7 +287,7 @@ public class GrailsDomainBinder
                 bindDiscriminatorProperty(root.getTable(), root, m);
             }
             // bind the sub classes
-            children.forEach(sub -> bindSubClass(sub, root, mappings, 
sessionFactoryBeanName, finalMapping,mappingCacheHolder, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, 
grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder));
+            children.forEach(sub -> bindSubClass(sub, root, mappings, 
sessionFactoryBeanName, finalMapping,mappingCacheHolder, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, 
grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder, 
singleTableSubclassBinder));
         }
 
         multiTenantFilterBinder.addMultiTenantFilterIfNecessary(entity, root, 
mappings, defaultColumnNameFetcher);
@@ -315,9 +317,9 @@ public class GrailsDomainBinder
                               PersistentClass parent,
                               @Nonnull InFlightMetadataCollector mappings,
                               String sessionFactoryBeanName
-                            , Mapping m, MappingCacheHolder 
mappingCacheHolder, DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenantFilterBinder, JoinedSubClassBinder joinedSubClassBinder, 
UnionSubclassBinder unionSubclassBinder) {
+                            , Mapping m, MappingCacheHolder 
mappingCacheHolder, DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenantFilterBinder, JoinedSubClassBinder joinedSubClassBinder, 
UnionSubclassBinder unionSubclassBinder, SingleTableSubclassBinder singleTabl 
[...]
         mappingCacheHolder.cacheMapping(sub);
-        Subclass subClass = createSubclassMapping(sub, parent, mappings, 
sessionFactoryBeanName, m, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder);
+        Subclass subClass = createSubclassMapping(sub, parent, mappings, 
sessionFactoryBeanName, m, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder, singleTableSubclassBinder);
 
 
         parent.addSubclass(subClass);
@@ -328,11 +330,11 @@ public class GrailsDomainBinder
         var children = sub.getChildEntities(dataSourceName);
         if (!children.isEmpty()) {
             // bind the sub classes
-            children.forEach(sub1 -> bindSubClass(sub1, subClass, mappings, 
sessionFactoryBeanName, m,mappingCacheHolder, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder ));
+            children.forEach(sub1 -> bindSubClass(sub1, subClass, mappings, 
sessionFactoryBeanName, m,mappingCacheHolder, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder, singleTableSubclassBinder ));
         }
     }
 
-    private @NonNull Subclass createSubclassMapping(@NonNull 
GrailsHibernatePersistentEntity subEntity, PersistentClass parent, @NonNull 
InFlightMetadataCollector mappings, String sessionFactoryBeanName, Mapping m, 
DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenan [...]
+    private @NonNull Subclass createSubclassMapping(@NonNull 
GrailsHibernatePersistentEntity subEntity, PersistentClass parent, @NonNull 
InFlightMetadataCollector mappings, String sessionFactoryBeanName, Mapping m, 
DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenan [...]
         Subclass subClass;
         subEntity.configureDerivedProperties();
         if (!m.getTablePerHierarchy() && !m.isTablePerConcreteClass()) {
@@ -347,8 +349,8 @@ public class GrailsDomainBinder
         }
         else {
             var singleTableSubclass = new SingleTableSubclass(parent, 
this.metadataBuildingContext);
-            
singleTableSubclass.setDiscriminatorValue(subEntity.getDiscriminatorValue());
-            bindSubClass(subEntity, singleTableSubclass, mappings, 
sessionFactoryBeanName, defaultColumnNameFetcher, grailsPropertyBinder, 
classBinder, propertyFromValueCreator, multiTenantFilterBinder, 
joinedSubClassBinder, unionSubclassBinder);
+
+            singleTableSubclassBinder.bindSubClass(subEntity, 
singleTableSubclass, mappings);
             subClass = singleTableSubclass;
         }
         
subClass.setBatchSize(Optional.ofNullable(m.getBatchSize()).orElse(-1));
@@ -370,17 +372,6 @@ public class GrailsDomainBinder
      * @param subClass The Hibernate SubClass instance
      * @param mappings The mappings instance
      */
-    private void bindSubClass(@Nonnull GrailsHibernatePersistentEntity sub, 
SingleTableSubclass subClass, @Nonnull InFlightMetadataCollector mappings,
-                                String sessionFactoryBeanName, 
DefaultColumnNameFetcher defaultColumnNameFetcher, GrailsPropertyBinder 
grailsPropertyBinder, ClassBinder classBinder, PropertyFromValueCreator 
propertyFromValueCreator, MultiTenantFilterBinder multiTenantFilterBinder, 
JoinedSubClassBinder joinedSubClassBinder, UnionSubclassBinder 
unionSubclassBinder) {
-        classBinder.bindClass(sub, subClass, mappings);
-
-        if (LOG.isDebugEnabled())
-            LOG.debug("Mapping subclass: " + subClass.getEntityName() +
-                    " -> " + subClass.getTable().getName());
-
-        // properties
-    }
-
     /**
      * Creates and binds the discriminator property used in 
table-per-hierarchy inheritance to
      * discriminate between sub class instances
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinder.java
new file mode 100644
index 0000000000..6236c0e5b4
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinder.java
@@ -0,0 +1,40 @@
+package org.grails.orm.hibernate.cfg.domainbinding.binder;
+
+import jakarta.annotation.Nonnull;
+import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity;
+import org.hibernate.boot.spi.InFlightMetadataCollector;
+import org.hibernate.mapping.SingleTableSubclass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Binds a sub-class using table-per-hierarchy inheritance mapping
+ *
+ * @since 7.0
+ */
+public class SingleTableSubclassBinder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(SingleTableSubclassBinder.class);
+
+    private final ClassBinder classBinder;
+
+    public SingleTableSubclassBinder(ClassBinder classBinder) {
+        this.classBinder = classBinder;
+    }
+
+    /**
+     * Binds a sub-class using table-per-hierarchy inheritance mapping
+     *
+     * @param sub      The Grails domain class instance representing the 
sub-class
+     * @param subClass The Hibernate SubClass instance
+     * @param mappings The mappings instance
+     */
+    public void bindSubClass(@Nonnull GrailsHibernatePersistentEntity sub, 
SingleTableSubclass subClass, @Nonnull InFlightMetadataCollector mappings) {
+        classBinder.bindClass(sub, subClass, mappings);
+        subClass.setDiscriminatorValue(sub.getDiscriminatorValue());
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Mapping subclass: " + subClass.getEntityName() +
+                    " -> " + subClass.getTable().getName());
+        }
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinderSpec.groovy
new file mode 100644
index 0000000000..9e76d4a527
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SingleTableSubclassBinderSpec.groovy
@@ -0,0 +1,61 @@
+package org.grails.orm.hibernate.cfg.domainbinding.binder
+
+import grails.gorm.annotation.Entity
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import org.hibernate.mapping.RootClass
+import org.hibernate.mapping.Table
+import org.hibernate.mapping.SingleTableSubclass
+
+/**
+ * Tests for SingleTableSubclassBinder using real entity classes.
+ */
+class SingleTableSubclassBinderSpec extends HibernateGormDatastoreSpec {
+
+    SingleTableSubclassBinder binder
+    ClassBinder classBinder = new ClassBinder()
+
+    void setup() {
+        binder = new SingleTableSubclassBinder(classBinder)
+    }
+
+    void "test bind single table subclass with real entities"() {
+        given:
+        def buildingContext = 
getGrailsDomainBinder().getMetadataBuildingContext()
+        def mappings = buildingContext.getMetadataCollector()
+        
+        // Register entities in mapping context
+        def rootEntity = createPersistentEntity(SingleTableSubClassRoot)
+        def subEntity = createPersistentEntity(SingleTableSubClassSub)
+        
+        // Setup Hibernate RootClass
+        def rootClass = new RootClass(buildingContext)
+        rootClass.setEntityName(SingleTableSubClassRoot.name)
+        def rootTable = new Table("ST_ROOT_TABLE")
+        rootTable.setName("ST_ROOT_TABLE")
+        rootClass.setTable(rootTable)
+        
+        // Setup SingleTableSubclass
+        def singleTableSubclass = new SingleTableSubclass(rootClass, 
buildingContext)
+        singleTableSubclass.setEntityName(SingleTableSubClassSub.name)
+
+        when:
+        binder.bindSubClass(subEntity, singleTableSubclass, mappings)
+
+        then:
+        singleTableSubclass.getTable() == rootTable
+        singleTableSubclass.getDiscriminatorValue() == "SUB_CLASS"
+    }
+}
+
+@Entity
+class SingleTableSubClassRoot {
+    Long id
+}
+
+@Entity
+class SingleTableSubClassSub extends SingleTableSubClassRoot {
+    String name
+    static mapping = {
+        discriminator "SUB_CLASS"
+    }
+}

Reply via email to