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

Reply via email to