This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch merge-hibernate6 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 1123f19b9275962a714c9449a20132ce3050787a Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Sep 30 21:02:22 2025 -0500 CompositeIdentifierToManyToOneBinder --- .../orm/hibernate/cfg/GrailsDomainBinder.java | 68 +---------- .../CompositeIdentifierToManyToOneBinder.java | 111 ++++++++++++++++++ .../PersistentPropertyToPropertyConfig.java | 4 +- ...CompositeIdentifierToManyToOneBinderSpec.groovy | 124 +++++++++++++++++++++ 4 files changed, 241 insertions(+), 66 deletions(-) diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java index d8df2b6449..624c407351 100644 --- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java @@ -28,7 +28,6 @@ import org.grails.datastore.mapping.model.types.Embedded; import org.grails.datastore.mapping.model.types.ManyToMany; import org.grails.datastore.mapping.model.types.TenantId; import org.grails.datastore.mapping.model.types.ToMany; -import org.grails.datastore.mapping.model.types.ToOne; import org.grails.orm.hibernate.cfg.domainbinding.ClassBinder; import org.grails.orm.hibernate.cfg.domainbinding.ColumnConfigToColumnBinder; import org.grails.orm.hibernate.cfg.domainbinding.ConfigureDerivedPropertiesConsumer; @@ -778,8 +777,7 @@ public class GrailsDomainBinder implements MetadataContributor { Mapping m = new HibernateEntityWrapper().getMappedForm(domainClass); if (m.hasCompositeIdentifier()) { CompositeIdentity ci = (CompositeIdentity) m.getIdentity(); - bindCompositeIdentifierToManyToOne(property, element, ci, domainClass, - EMPTY_PATH, sessionFactoryBeanName); + new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne(property, element, ci, domainClass, EMPTY_PATH); } else { if (joinColumnMappingOptional.isPresent()) { @@ -856,7 +854,7 @@ public class GrailsDomainBinder implements MetadataContributor { if ((shouldCollectionBindWithJoinColumn((ToMany) property) && hasCompositeIdentifier) || (hasCompositeIdentifier && ( property instanceof ManyToMany))) { CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); - bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH, sessionFactoryBeanName); + new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH); } else { // set type @@ -2054,7 +2052,7 @@ public class GrailsDomainBinder implements MetadataContributor { boolean isComposite = mapping.hasCompositeIdentifier(); if (isComposite) { CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); - bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path, sessionFactoryBeanName); + new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path); } else { //TODO NOT TESTED @@ -2097,66 +2095,6 @@ public class GrailsDomainBinder implements MetadataContributor { } } - private void bindCompositeIdentifierToManyToOne(Association property, - SimpleValue value, CompositeIdentity compositeId, PersistentEntity refDomainClass, - String path, String sessionFactoryBeanName) { - String[] propertyNames = compositeId.getPropertyNames(); - PropertyConfig config = new PersistentPropertyToPropertyConfig().apply(property); - - List<ColumnConfig> columns = config.getColumns(); - int i = columns.size(); - int expectedForeignKeyColumnLength = new ForeignKeyColumnCountCalculator().calculateForeignKeyColumnCount(refDomainClass, propertyNames); - if (i != expectedForeignKeyColumnLength) { - int j = 0; - for (String propertyName : propertyNames) { - ColumnConfig cc; - // if a column configuration exists in the mapping use it - if(j < i) { - cc = columns.get(j++); - } - // otherwise create a new one to represent the composite column - else { - cc = new ColumnConfig(); - } - // if the name is null then configure the name by convention - if(cc.getName() == null) { - // use the referenced table name as a prefix - String prefix = new TableNameFetcher(getNamingStrategy()).getTableName(refDomainClass); - PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); - - // if the referenced property is a ToOne and it has a composite id - // then a column is needed for each property that forms the composite id - if(referencedProperty instanceof ToOne) { - ToOne toOne = (ToOne) referencedProperty; - PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); - if(compositeIdentity != null) { - for (PersistentProperty cip : compositeIdentity) { - // for each property of a composite id by default we use the table name and the property name as a prefix - String string = getNamingStrategy().resolveColumnName(referencedProperty.getName()); - String compositeIdPrefix = new BackticksRemover().apply(prefix) + UNDERSCORE + new BackticksRemover().apply(string); - - String suffix = new DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(cip); - String finalColumnName = new BackticksRemover().apply(compositeIdPrefix) + UNDERSCORE + new BackticksRemover().apply(suffix); - cc = new ColumnConfig(); - cc.setName(finalColumnName); - columns.add(cc); - } - continue; - } - } - - String suffix = new DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(referencedProperty); - String finalColumnName = new BackticksRemover().apply(prefix) + UNDERSCORE + new BackticksRemover().apply(suffix); - cc.setName(finalColumnName); - columns.add(cc); - } - } - } - new PersistentPropertyToPropertyConfig().apply(property); - // set type - new SimpleValueBinder(namingStrategy).bindSimpleValue(property, null, value, path); - } - // each property may consist of one or many columns (due to composite ids) so in order to get the // number of columns required for a column key we have to perform the calculation here diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java new file mode 100644 index 0000000000..d572808661 --- /dev/null +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java @@ -0,0 +1,111 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import java.util.List; + +import org.hibernate.mapping.SimpleValue; + +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.ToOne; +import org.grails.orm.hibernate.cfg.ColumnConfig; +import org.grails.orm.hibernate.cfg.CompositeIdentity; +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; +import org.grails.orm.hibernate.cfg.PropertyConfig; + +import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.UNDERSCORE; + +public class CompositeIdentifierToManyToOneBinder { + private final ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator; + private final TableNameFetcher tableNameFetcher; + private final PersistentEntityNamingStrategy namingStrategy; + private final DefaultColumnNameFetcher defaultColumnNameFetcher; + private final PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig; + private final BackticksRemover backticksRemover; + private final SimpleValueBinder simpleValueBinder; + + public CompositeIdentifierToManyToOneBinder(PersistentEntityNamingStrategy namingStrategy){ + this.foreignKeyColumnCountCalculator = new ForeignKeyColumnCountCalculator(); + this.tableNameFetcher = new TableNameFetcher(namingStrategy); + this.namingStrategy = namingStrategy; + this.defaultColumnNameFetcher = new DefaultColumnNameFetcher(namingStrategy); + this.persistentPropertyToPropertyConfig = new PersistentPropertyToPropertyConfig(); + this.backticksRemover = new BackticksRemover(); + this.simpleValueBinder = new SimpleValueBinder(namingStrategy); + + } + + protected CompositeIdentifierToManyToOneBinder(ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator + , TableNameFetcher tableNameFetcher + , PersistentEntityNamingStrategy namingStrategy + , DefaultColumnNameFetcher defaultColumnNameFetcher + , PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig + , BackticksRemover backticksRemover, SimpleValueBinder simpleValueBinder) { + this.foreignKeyColumnCountCalculator = foreignKeyColumnCountCalculator; + this.tableNameFetcher =tableNameFetcher; + this.namingStrategy = namingStrategy; + this.defaultColumnNameFetcher = defaultColumnNameFetcher; + this.persistentPropertyToPropertyConfig = persistentPropertyToPropertyConfig; + this.backticksRemover = backticksRemover; + this.simpleValueBinder = simpleValueBinder; + } + + public void bindCompositeIdentifierToManyToOne(Association property, + SimpleValue value, + CompositeIdentity compositeId, + PersistentEntity refDomainClass, + String path) { + String[] propertyNames = compositeId.getPropertyNames(); + + List<ColumnConfig> columns = persistentPropertyToPropertyConfig.apply(property).getColumns(); + int i = columns.size(); + int expectedForeignKeyColumnLength = foreignKeyColumnCountCalculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames); + if (i != expectedForeignKeyColumnLength) { + int j = 0; + for (String propertyName : propertyNames) { + ColumnConfig cc; + // if a column configuration exists in the mapping use it + if (j < i) { + cc = columns.get(j++); + } + // otherwise create a new one to represent the composite column + else { + cc = new ColumnConfig(); + } + // if the name is null then configure the name by convention + if (cc.getName() == null) { + // use the referenced table name as a prefix + String prefix = tableNameFetcher.getTableName(refDomainClass); + PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); + + // if the referenced property is a ToOne and it has a composite id + // then a column is needed for each property that forms the composite id + if (referencedProperty instanceof ToOne toOne) { + PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); + if (compositeIdentity != null) { + for (PersistentProperty cip : compositeIdentity) { + // for each property of a composite id by default we use the table name and the property name as a prefix + String string = namingStrategy.resolveColumnName(referencedProperty.getName()); + String compositeIdPrefix = backticksRemover.apply(prefix) + UNDERSCORE + backticksRemover.apply(string); + + String suffix = defaultColumnNameFetcher.getDefaultColumnName(cip); + String finalColumnName = backticksRemover.apply(compositeIdPrefix) + UNDERSCORE + backticksRemover.apply(suffix); + cc = new ColumnConfig(); + cc.setName(finalColumnName); + columns.add(cc); + } + continue; + } + } + + String suffix = defaultColumnNameFetcher.getDefaultColumnName(referencedProperty); + String finalColumnName = backticksRemover.apply(prefix) + UNDERSCORE + backticksRemover.apply(suffix); + cc.setName(finalColumnName); + columns.add(cc); + } + } + } + // set type + simpleValueBinder.bindSimpleValue(property, null, value, path); + } +} diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java index 53be315bf3..3272159ae8 100644 --- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java @@ -7,9 +7,11 @@ import org.hibernate.MappingException; import java.util.Optional; import java.util.function.Function; +import jakarta.annotation.Nonnull; + public class PersistentPropertyToPropertyConfig implements Function<PersistentProperty, PropertyConfig> { @Override - public PropertyConfig apply(PersistentProperty persistentProperty) { + @Nonnull public PropertyConfig apply(PersistentProperty persistentProperty) { return Optional.ofNullable(persistentProperty) .map(PersistentProperty::getMappedForm) .map(PropertyConfig.class::cast) diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy new file mode 100644 index 0000000000..61bfa0d732 --- /dev/null +++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy @@ -0,0 +1,124 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.ToOne +import org.grails.orm.hibernate.cfg.ColumnConfig +import org.grails.orm.hibernate.cfg.CompositeIdentity +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy +import org.grails.orm.hibernate.cfg.PropertyConfig +import org.hibernate.mapping.SimpleValue +import spock.lang.Specification + +class CompositeIdentifierToManyToOneBinderSpec extends Specification { + + def "Test bindCompositeIdentifierToManyToOne with nested composite ID"() { + given: + // 1. Stub all dependencies for the protected constructor + def calculator = Stub(ForeignKeyColumnCountCalculator) + def tableNameFetcher = Stub(TableNameFetcher) + def namingStrategy = Stub(PersistentEntityNamingStrategy) + def columnNameFetcher = Stub(DefaultColumnNameFetcher) + def propertyConfigConverter = Stub(PersistentPropertyToPropertyConfig) + def backticksRemover = Stub(BackticksRemover) + def simpleValueBinder = Mock(SimpleValueBinder) // Use Mock to verify interaction + + // Instantiate the binder with stubs + def binder = new CompositeIdentifierToManyToOneBinder(calculator, tableNameFetcher, namingStrategy, columnNameFetcher, propertyConfigConverter, backticksRemover, simpleValueBinder) + + // 2. Set up stubs for the method arguments + def association = Stub(ToOne) + def value = Stub(SimpleValue) + def refDomainClass = Stub(PersistentEntity) + def path = "/test" + + // Use a real CompositeIdentity object to avoid final method mocking issues + def propertyNames = ["nestedEntity"] as String[] + def compositeId = new CompositeIdentity() + compositeId.setPropertyNames(propertyNames) + + // 3. Define the nested composite key scenario + def propertyConfig = new PropertyConfig() + propertyConfigConverter.apply(association) >> propertyConfig + + calculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames) >> 2 + + def nestedEntityProp = Stub(ToOne) + refDomainClass.getPropertyByName("nestedEntity") >> nestedEntityProp + nestedEntityProp.name >> "nestedEntity" + + def nestedAssociatedEntity = Stub(PersistentEntity) + nestedEntityProp.getAssociatedEntity() >> nestedAssociatedEntity + + def nestedPartA = Stub(PersistentProperty) + def nestedPartB = Stub(PersistentProperty) + def perArray = [nestedPartA, nestedPartB] as PersistentProperty[] + nestedAssociatedEntity.getCompositeIdentity() >> perArray + + // 4. Mock the behavior of the dependency methods + tableNameFetcher.getTableName(refDomainClass) >> "ref_table" + namingStrategy.resolveColumnName("nestedEntity") >> "nested_entity_col" + columnNameFetcher.getDefaultColumnName(nestedPartA) >> "part_a_col" + columnNameFetcher.getDefaultColumnName(nestedPartB) >> "part_b_col" + + // Make backticks remover pass through the values for simplicity + backticksRemover.apply(_) >> { String s -> s } + + when: + binder.bindCompositeIdentifierToManyToOne(association, value, compositeId, refDomainClass, path) + + then: + // 5. Verify the final generated column names + def finalColumns = propertyConfig.getColumns() + finalColumns.size() == 2 + finalColumns[0].getName() == "ref_table_nested_entity_col_part_a_col" + finalColumns[1].getName() == "ref_table_nested_entity_col_part_b_col" + + and: // 6. Verify the call to the simple value binder + 1 * simpleValueBinder.bindSimpleValue(association, null, value, path) + } + + def "Test bindCompositeIdentifierToManyToOne when column count matches"() { + given: + // 1. Use Mocks for dependencies that require interaction verification + def calculator = Stub(ForeignKeyColumnCountCalculator) + def tableNameFetcher = Mock(TableNameFetcher) + def namingStrategy = Mock(PersistentEntityNamingStrategy) + def columnNameFetcher = Mock(DefaultColumnNameFetcher) + def propertyConfigConverter = Stub(PersistentPropertyToPropertyConfig) + def backticksRemover = Mock(BackticksRemover) + def simpleValueBinder = Mock(SimpleValueBinder) + + def binder = new CompositeIdentifierToManyToOneBinder(calculator, tableNameFetcher, namingStrategy, columnNameFetcher, propertyConfigConverter, backticksRemover, simpleValueBinder) + + // 2. Set up arguments + def association = Stub(ToOne) + def value = Stub(SimpleValue) + def compositeId = new CompositeIdentity() + compositeId.setPropertyNames(["prop1", "prop2"] as String[]) + def refDomainClass = Stub(PersistentEntity) + def path = "/test" + + // 3. Set up the "match" condition + def propertyConfig = new PropertyConfig() + propertyConfig.getColumns().add(new ColumnConfig()) + propertyConfig.getColumns().add(new ColumnConfig()) + propertyConfigConverter.apply(association) >> propertyConfig + + // The calculated length is the same as the number of columns already in the config + calculator.calculateForeignKeyColumnCount(refDomainClass, _ as String[]) >> 2 + + when: + binder.bindCompositeIdentifierToManyToOne(association, value, compositeId, refDomainClass, path) + + then: + // 4. Verify the column name generation logic is skipped + 0 * tableNameFetcher._ + 0 * namingStrategy._ + 0 * columnNameFetcher._ + 0 * backticksRemover._ + + and: // 5. Verify the simple value binder is still called + 1 * simpleValueBinder.bindSimpleValue(association, null, value, path) + } +}
