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