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" + } +}
