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 5052e3777fe4706c843f8b295ad7416d4a50dd39
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Feb 15 11:45:38 2026 -0600

    Refactor union subclass binding to a dedicated binder class
    
    - Create UnionSubclassBinder to handle table-per-concrete-class mapping.
    - Update GrailsDomainBinder to use UnionSubclassBinder as a local 
dependency.
    - Add UnionSubclassBinderSpec using real entity classes for comprehensive 
testing.
---
 .../orm/hibernate/cfg/GrailsDomainBinder.java      | 48 ++++------------
 .../domainbinding/binder/UnionSubclassBinder.java  | 65 +++++++++++++++++++++
 .../binder/UnionSubclassBinderSpec.groovy          | 66 ++++++++++++++++++++++
 3 files changed, 143 insertions(+), 36 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 b53197732b..677c20ade4 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.UnionSubclassBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.binder.VersionBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder;
@@ -230,12 +231,13 @@ public class GrailsDomainBinder
         VersionBinder versionBinder = new 
VersionBinder(metadataBuildingContext, simpleValueBinder, propertyBinder, 
BasicValue::new);
         MultiTenantFilterBinder multiTenantFilterBinder = new 
MultiTenantFilterBinder();
         JoinedSubClassBinder joinedSubClassBinder = new 
JoinedSubClassBinder(metadataBuildingContext, namingStrategy, new 
SimpleValueColumnBinder(), columnNameForPropertyAndPathFetcher, classBinder);
+        UnionSubclassBinder unionSubclassBinder = new 
UnionSubclassBinder(metadataBuildingContext, namingStrategy, 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));
+                .forEach(hibernatePersistentEntity -> 
bindRoot(hibernatePersistentEntity, metadataCollector, sessionFactoryName, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, identityBinder, 
versionBinder, grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder));
     }
 
 
@@ -266,7 +268,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;
@@ -283,7 +285,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));
+            children.forEach(sub -> bindSubClass(sub, root, mappings, 
sessionFactoryBeanName, finalMapping,mappingCacheHolder, 
defaultColumnNameFetcher, columnNameForPropertyAndPathFetcher, 
grailsPropertyBinder, classBinder, propertyFromValueCreator, 
multiTenantFilterBinder, joinedSubClassBinder, unionSubclassBinder));
         }
 
         multiTenantFilterBinder.addMultiTenantFilterIfNecessary(entity, root, 
mappings, defaultColumnNameFetcher);
@@ -313,9 +315,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) {
+                            , Mapping m, MappingCacheHolder 
mappingCacheHolder, DefaultColumnNameFetcher defaultColumnNameFetcher, 
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher, 
GrailsPropertyBinder grailsPropertyBinder, ClassBinder classBinder, 
PropertyFromValueCreator propertyFromValueCreator, MultiTenantFilterBinder 
multiTenantFilterBinder, JoinedSubClassBinder joinedSubClassBinder, 
UnionSubclassBinder unionSubclassBinder) {
         mappingCacheHolder.cacheMapping(sub);
-        Subclass subClass = createSubclassMapping(sub, parent, mappings, 
sessionFactoryBeanName, m, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder);
+        Subclass subClass = createSubclassMapping(sub, parent, mappings, 
sessionFactoryBeanName, m, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder);
 
 
         parent.addSubclass(subClass);
@@ -326,11 +328,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 ));
+            children.forEach(sub1 -> bindSubClass(sub1, subClass, mappings, 
sessionFactoryBeanName, m,mappingCacheHolder, defaultColumnNameFetcher, 
columnNameForPropertyAndPathFetcher, grailsPropertyBinder, classBinder, 
propertyFromValueCreator, multiTenantFilterBinder, joinedSubClassBinder, 
unionSubclassBinder ));
         }
     }
 
-    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()) {
@@ -340,13 +342,13 @@ public class GrailsDomainBinder
         }
         else if(m.isTablePerConcreteClass()) {
             var union  = new UnionSubclass(parent, 
this.metadataBuildingContext);
-            bindUnionSubclass(subEntity,  union, mappings, 
sessionFactoryBeanName, grailsPropertyBinder, classBinder, 
propertyFromValueCreator);
+            unionSubclassBinder.bindUnionSubclass(subEntity, union, mappings);
             subClass = union;
         }
         else {
             var singleTableSubclass = new SingleTableSubclass(parent, 
this.metadataBuildingContext);
             
singleTableSubclass.setDiscriminatorValue(subEntity.getDiscriminatorValue());
-            bindSubClass(subEntity, singleTableSubclass, mappings, 
sessionFactoryBeanName, defaultColumnNameFetcher, grailsPropertyBinder, 
classBinder, propertyFromValueCreator, multiTenantFilterBinder, 
joinedSubClassBinder);
+            bindSubClass(subEntity, singleTableSubclass, mappings, 
sessionFactoryBeanName, defaultColumnNameFetcher, grailsPropertyBinder, 
classBinder, propertyFromValueCreator, multiTenantFilterBinder, 
joinedSubClassBinder, unionSubclassBinder);
             subClass = singleTableSubclass;
         }
         
subClass.setBatchSize(Optional.ofNullable(m.getBatchSize()).orElse(-1));
@@ -361,32 +363,6 @@ public class GrailsDomainBinder
     }
 
 
-    private void bindUnionSubclass(@Nonnull GrailsHibernatePersistentEntity 
subClass, UnionSubclass unionSubclass,
-                                  @Nonnull InFlightMetadataCollector mappings, 
String sessionFactoryBeanName, GrailsPropertyBinder grailsPropertyBinder, 
ClassBinder classBinder, PropertyFromValueCreator propertyFromValueCreator) 
throws MappingException {
-        classBinder.bindClass(subClass, unionSubclass, mappings);
-
-        String schema = subClass.getSchema(mappings);
-        String catalog = subClass.getCatalog(mappings);
-
-        Table denormalizedSuperTable = 
unionSubclass.getSuperclass().getTable();
-        Table mytable = mappings.addDenormalizedTable(
-                schema,
-                catalog,
-                new 
TableNameFetcher(getNamingStrategy()).getTableName(subClass),
-                Boolean.TRUE.equals(unionSubclass.isAbstract()),
-                null,
-                denormalizedSuperTable, metadataBuildingContext
-        );
-        unionSubclass.setTable( mytable );
-        unionSubclass.setClassName(subClass.getName());
-
-        LOG.info(
-                "Mapping union-subclass: " + unionSubclass.getEntityName() +
-                        " -> " + unionSubclass.getTable().getName()
-        );
-
-
-    }
     /**
      * Binds a sub-class using table-per-hierarchy inheritance mapping
      *
@@ -395,7 +371,7 @@ public class GrailsDomainBinder
      * @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) {
+                                String sessionFactoryBeanName, 
DefaultColumnNameFetcher defaultColumnNameFetcher, GrailsPropertyBinder 
grailsPropertyBinder, ClassBinder classBinder, PropertyFromValueCreator 
propertyFromValueCreator, MultiTenantFilterBinder multiTenantFilterBinder, 
JoinedSubClassBinder joinedSubClassBinder, UnionSubclassBinder 
unionSubclassBinder) {
         classBinder.bindClass(sub, subClass, mappings);
 
         if (LOG.isDebugEnabled())
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinder.java
new file mode 100644
index 0000000000..7982ed1005
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinder.java
@@ -0,0 +1,65 @@
+package org.grails.orm.hibernate.cfg.domainbinding.binder;
+
+import jakarta.annotation.Nonnull;
+import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.domainbinding.util.TableNameFetcher;
+import org.hibernate.MappingException;
+import org.hibernate.boot.spi.InFlightMetadataCollector;
+import org.hibernate.boot.spi.MetadataBuildingContext;
+import org.hibernate.mapping.Table;
+import org.hibernate.mapping.UnionSubclass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Binds a union sub-class mapping using table-per-concrete-class
+ *
+ * @since 7.0
+ */
+public class UnionSubclassBinder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(UnionSubclassBinder.class);
+
+    private final MetadataBuildingContext metadataBuildingContext;
+    private final PersistentEntityNamingStrategy namingStrategy;
+    private final ClassBinder classBinder;
+
+    public UnionSubclassBinder(MetadataBuildingContext 
metadataBuildingContext, PersistentEntityNamingStrategy namingStrategy, 
ClassBinder classBinder) {
+        this.metadataBuildingContext = metadataBuildingContext;
+        this.namingStrategy = namingStrategy;
+        this.classBinder = classBinder;
+    }
+
+    /**
+     * Binds a union sub-class mapping using table-per-concrete-class
+     *
+     * @param subClass       The Grails sub class
+     * @param unionSubclass  The Hibernate UnionSubclass object
+     * @param mappings       The mappings Object
+     */
+    public void bindUnionSubclass(@Nonnull GrailsHibernatePersistentEntity 
subClass, UnionSubclass unionSubclass,
+                                   @Nonnull InFlightMetadataCollector 
mappings) throws MappingException {
+        classBinder.bindClass(subClass, unionSubclass, mappings);
+
+        String schema = subClass.getSchema(mappings);
+        String catalog = subClass.getCatalog(mappings);
+
+        Table denormalizedSuperTable = 
unionSubclass.getSuperclass().getTable();
+        Table mytable = mappings.addDenormalizedTable(
+                schema,
+                catalog,
+                new TableNameFetcher(namingStrategy).getTableName(subClass),
+                Boolean.TRUE.equals(unionSubclass.isAbstract()),
+                null,
+                denormalizedSuperTable, metadataBuildingContext
+        );
+        unionSubclass.setTable( mytable );
+        unionSubclass.setClassName(subClass.getName());
+
+        if (LOG.isInfoEnabled()) {
+            LOG.info("Mapping union-subclass: " + 
unionSubclass.getEntityName() +
+                    " -> " + unionSubclass.getTable().getName());
+        }
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinderSpec.groovy
new file mode 100644
index 0000000000..b9f9da7c92
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/UnionSubclassBinderSpec.groovy
@@ -0,0 +1,66 @@
+package org.grails.orm.hibernate.cfg.domainbinding.binder
+
+import grails.gorm.annotation.Entity
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity
+import org.hibernate.mapping.RootClass
+import org.hibernate.mapping.Table
+import org.hibernate.mapping.UnionSubclass
+
+/**
+ * Tests for UnionSubclassBinder using real entity classes.
+ */
+class UnionSubclassBinderSpec extends HibernateGormDatastoreSpec {
+
+    UnionSubclassBinder binder
+    ClassBinder classBinder = new ClassBinder()
+
+    void setup() {
+        def buildingContext = 
getGrailsDomainBinder().getMetadataBuildingContext()
+        def namingStrategy = getGrailsDomainBinder().getNamingStrategy()
+        binder = new UnionSubclassBinder(buildingContext, namingStrategy, 
classBinder)
+    }
+
+    void "test bind union subclass with real entities"() {
+        given:
+        def buildingContext = 
getGrailsDomainBinder().getMetadataBuildingContext()
+        def mappings = buildingContext.getMetadataCollector()
+        
+        // Register entities in mapping context
+        def rootEntity = createPersistentEntity(UnionSubClassRoot)
+        def subEntity = createPersistentEntity(UnionSubClassSub)
+        
+        // Setup Hibernate RootClass
+        def rootClass = new RootClass(buildingContext)
+        rootClass.setEntityName(UnionSubClassRoot.name)
+        def rootTable = new Table("US_ROOT_TABLE")
+        rootTable.setName("US_ROOT_TABLE")
+        rootClass.setTable(rootTable)
+        
+        // Setup UnionSubclass
+        def unionSubclass = new UnionSubclass(rootClass, buildingContext)
+        unionSubclass.setEntityName(UnionSubClassSub.name)
+
+        when:
+        binder.bindUnionSubclass(subEntity, unionSubclass, mappings)
+
+        then:
+        unionSubclass.getTable() != null
+        unionSubclass.getTable().getName() != "US_ROOT_TABLE"
+        unionSubclass.getClassName() == UnionSubClassSub.name
+    }
+}
+
+@Entity
+class UnionSubClassRoot {
+    Long id
+}
+
+@Entity
+class UnionSubClassSub extends UnionSubClassRoot {
+    String name
+    static mapping = {
+        tablePerHierarchy false
+        tablePerConcreteClass true
+    }
+}

Reply via email to