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 2a4b2892b01c2760f1e0c1e56053a1e1a947bd04
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Oct 5 21:04:39 2025 -0500

    ManyToOne
---
 .../orm/hibernate/cfg/GrailsDomainBinder.java      | 106 +-----------
 .../cfg/domainbinding/ManyToOneBinder.java         | 115 +++++++++++++
 .../cfg/domainbinding/ManyToOneValuesBinder.java   |  37 ++++
 .../domainbinding/SimpleValueColumnFetcher.java    |  12 ++
 .../cfg/domainbinding/ManyToOneBinderSpec.groovy   | 190 +++++++++++++++++++++
 .../domainbinding/ManyToOneValuesBinderSpec.groovy |  54 ++++++
 6 files changed, 416 insertions(+), 98 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 f311f83057..30cb3922d6 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
@@ -151,7 +151,6 @@ public class GrailsDomainBinder implements 
MetadataContributor {
     private Closure defaultMapping;
     private PersistentEntityNamingStrategy namingStrategy;
     private MetadataBuildingContext metadataBuildingContext;
-    private NamingStrategyWrapper namingStrategyWrapper;
 
 
     public JdbcEnvironment getJdbcEnvironment() {
@@ -271,7 +270,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         new SimpleValueColumnBinder().bindSimpleValue(value, type, 
columnName1, true);
         PropertyConfig mappedForm = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
         if (mappedForm.getIndexColumn() != null) {
-            Column column = getColumnForSimpleValue(value);
+            Column column = new 
SimpleValueColumnFetcher().getColumnForSimpleValue(value);
             ColumnConfig columnConfig = 
getSingleColumnConfig(mappedForm.getIndexColumn());
             new ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, 
columnConfig, mappedForm);
         }
@@ -765,7 +764,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
 
                 new SimpleValueColumnBinder().bindSimpleValue(element, 
typeName, columnName, true);
                 if (joinColumnMappingOptional.isPresent()) {
-                    Column column = getColumnForSimpleValue(element);
+                    Column column = new 
SimpleValueColumnFetcher().getColumnForSimpleValue(element);
                     ColumnConfig columnConfig = 
joinColumnMappingOptional.get();
                     final PropertyConfig mappedForm = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
                     new 
ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, columnConfig, 
mappedForm);
@@ -797,10 +796,6 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         new 
CollectionForPropertyConfigBinder().bindCollectionForPropertyConfig(collection, 
config);
     }
 
-    private Column getColumnForSimpleValue(SimpleValue element) {
-        return element.getColumns().iterator().next();
-    }
-
     private boolean shouldCollectionBindWithJoinColumn(ToMany property) {
         PropertyConfig config = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
         JoinTable jt = config.getJoinTable();
@@ -982,7 +977,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
      */
     private void bindManyToMany(Association property, ManyToOne element,
                                   InFlightMetadataCollector mappings, String 
sessionFactoryBeanName) {
-        bindManyToOne(property, element, EMPTY_PATH, mappings, 
sessionFactoryBeanName);
+        new ManyToOneBinder(namingStrategy).bindManyToOne(property, element, 
EMPTY_PATH);
         element.setReferencedEntityName(property.getOwner().getName());
     }
 
@@ -1812,7 +1807,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
                         LOG.debug("[GrailsDomainBinder] Binding property [" + 
currentGrailsProp.getName() + "] as ManyToOne");
 
                     value = new ManyToOne(metadataBuildingContext, table);
-                    bindManyToOne((Association) currentGrailsProp, (ManyToOne) 
value, EMPTY_PATH, mappings, sessionFactoryBeanName);
+                    new 
ManyToOneBinder(namingStrategy).bindManyToOne((Association) currentGrailsProp, 
(ManyToOne) value, EMPTY_PATH);
                 }
                 else if (currentGrailsProp instanceof 
org.grails.datastore.mapping.model.types.OneToOne && userType == null) {
                     if (LOG.isDebugEnabled()) {
@@ -1835,7 +1830,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
                         }
                         else {
                             value = new ManyToOne(metadataBuildingContext, 
table);
-                            bindManyToOne((Association) currentGrailsProp, 
(ManyToOne) value, EMPTY_PATH, mappings, sessionFactoryBeanName);
+                            new 
ManyToOneBinder(namingStrategy).bindManyToOne((Association) currentGrailsProp, 
(ManyToOne) value, EMPTY_PATH);
                         }
                     }
                 }
@@ -1960,7 +1955,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
                 LOG.debug("[GrailsDomainBinder] Binding property [" + 
currentGrailsProp.getName() + "] as ManyToOne");
 
             value = new ManyToOne(metadataBuildingContext, table);
-            bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, 
path, mappings, sessionFactoryBeanName);
+            new ManyToOneBinder(namingStrategy).bindManyToOne((Association) 
currentGrailsProp, (ManyToOne) value, path);
         } else if (currentGrailsProp instanceof 
org.grails.datastore.mapping.model.types.OneToOne) {
             if (LOG.isDebugEnabled())
                 LOG.debug("[GrailsDomainBinder] Binding property [" + 
currentGrailsProp.getName() + "] as OneToOne");
@@ -1971,7 +1966,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
             }
             else {
                 value = new ManyToOne(metadataBuildingContext, table);
-                bindManyToOne((Association) currentGrailsProp, (ManyToOne) 
value, path, mappings, sessionFactoryBeanName);
+                new 
ManyToOneBinder(namingStrategy).bindManyToOne((Association) currentGrailsProp, 
(ManyToOne) value, path);
             }
         }
         else if (currentGrailsProp instanceof Embedded) {
@@ -2034,62 +2029,6 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         one.setIgnoreNotFound(true);
     }
 
-    /**
-     * Binds a many-to-one relationship to the
-     *
-     */
-    @SuppressWarnings("unchecked")
-    private void bindManyToOne(Association property, ManyToOne manyToOne,
-                                 String path, InFlightMetadataCollector 
mappings, String sessionFactoryBeanName) {
-        bindManyToOneValues(property, manyToOne);
-        PersistentEntity refDomainClass = property instanceof ManyToMany ? 
property.getOwner() : property.getAssociatedEntity();
-        Mapping mapping = new 
HibernateEntityWrapper().getMappedForm(refDomainClass);
-
-        boolean isComposite = mapping.hasCompositeIdentifier();
-        if (isComposite) {
-            CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity();
-            new 
CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne(property,
 manyToOne, ci, refDomainClass, path);
-        }
-        else {
-            //TODO NOT TESTED
-            if (property.isCircular() && (property instanceof ManyToMany)) {
-                PropertyConfig pc = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
-
-                if (pc.getColumns().isEmpty()) {
-                    mapping.getColumns().put(property.getName(), pc);
-                }
-                if (!pc.hasJoinKeyMapping()) {
-                    JoinTable jt = new JoinTable();
-                    final ColumnConfig columnConfig = new ColumnConfig();
-                    
columnConfig.setName(getNamingStrategy().resolveColumnName(property.getName()) 
+ UNDERSCORE + FOREIGN_KEY_SUFFIX);
-                    jt.setKey(columnConfig);
-                    pc.setJoinTable(jt);
-                }
-                // set type
-                new 
SimpleValueBinder(namingStrategy).bindSimpleValue(property, null, manyToOne, 
path);
-            }
-            else {
-                // bind column
-                // set type
-                new 
SimpleValueBinder(namingStrategy).bindSimpleValue(property, null, manyToOne, 
path);
-            }
-        }
-
-        PropertyConfig config = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
-        if ((property instanceof 
org.grails.datastore.mapping.model.types.OneToOne) && !isComposite) {
-            manyToOne.setAlternateUniqueKey(true);
-            Column c = getColumnForSimpleValue(manyToOne);
-            if (!config.isUniqueWithinGroup()) {
-                c.setUnique(config.isUnique());
-            }
-            else {
-                if (property.isBidirectional() && 
property.getInverseSide().isHasOne()) {
-                    c.setUnique(true);
-                }
-            }
-        }
-    }
-
     // 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
 
@@ -2116,7 +2055,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         oneToOne.setPropertyName(property.getName());
         oneToOne.setReferenceToPrimaryKey(false);
 
-        bindOneToOneInternal(property, oneToOne, path);
+        //no-op, for subclasses to extend
 
         if (hasOne) {
             //TODO NOT TESTED
@@ -2128,35 +2067,6 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         }
     }
 
-    private void 
bindOneToOneInternal(org.grails.datastore.mapping.model.types.OneToOne 
property, OneToOne oneToOne, String path) {
-        //no-op, for subclasses to extend
-    }
-
-    /**
-     */
-    private void 
bindManyToOneValues(org.grails.datastore.mapping.model.types.Association 
property, ManyToOne manyToOne) {
-        PropertyConfig config = new 
PersistentPropertyToPropertyConfig().toPropertyConfig(property);
-
-        if (config.getFetchMode() != null) {
-            manyToOne.setFetchMode(config.getFetchMode());
-        }
-        else {
-            manyToOne.setFetchMode(FetchMode.DEFAULT);
-        }
-
-
-        manyToOne.setLazy(Optional.ofNullable(config)
-                .map(org.grails.datastore.mapping.config.Property::getLazy)
-                .orElse(property != null));
-
-        if (config != null) {
-            manyToOne.setIgnoreNotFound(config.getIgnoreNotFound());
-        }
-
-        // set referenced entity
-        
manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName());
-    }
-
     private void bindVersion(PersistentProperty version, RootClass entity,
                                InFlightMetadataCollector mappings, String 
sessionFactoryBeanName) {
 
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinder.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinder.java
new file mode 100644
index 0000000000..614261fcf5
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinder.java
@@ -0,0 +1,115 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import org.hibernate.MappingException;
+import org.hibernate.boot.spi.InFlightMetadataCollector;
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.ManyToOne;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.model.types.ManyToMany;
+import org.grails.orm.hibernate.cfg.ColumnConfig;
+import org.grails.orm.hibernate.cfg.CompositeIdentity;
+import org.grails.orm.hibernate.cfg.JoinTable;
+import org.grails.orm.hibernate.cfg.Mapping;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+import static 
org.grails.orm.hibernate.cfg.GrailsDomainBinder.FOREIGN_KEY_SUFFIX;
+
+public class ManyToOneBinder {
+
+    private final PersistentEntityNamingStrategy namingStrategy;
+    private final SimpleValueBinder simpleValueBinder;
+    private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+    private final ManyToOneValuesBinder manyToOneValuesBinder;
+    private final CompositeIdentifierToManyToOneBinder 
compositeIdentifierToManyToOneBinder;
+    private final SimpleValueColumnFetcher simpleValueColumnFetcher;
+    private final HibernateEntityWrapper hibernateEntityWrapper;
+
+    public ManyToOneBinder(PersistentEntityNamingStrategy namingStrategy) {
+        this.namingStrategy = namingStrategy;
+        this.simpleValueBinder = new SimpleValueBinder(namingStrategy);
+        this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+        this.manyToOneValuesBinder = new ManyToOneValuesBinder();
+        this.compositeIdentifierToManyToOneBinder = new 
CompositeIdentifierToManyToOneBinder(namingStrategy);
+        this.simpleValueColumnFetcher = new SimpleValueColumnFetcher();
+        this.hibernateEntityWrapper = new HibernateEntityWrapper();
+    }
+
+    protected  ManyToOneBinder(PersistentEntityNamingStrategy namingStrategy
+            , SimpleValueBinder simpleValueBinder
+    , PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig
+    , ManyToOneValuesBinder manyToOneValuesBinder
+    , CompositeIdentifierToManyToOneBinder compositeIdentifierToManyToOneBinder
+    , SimpleValueColumnFetcher simpleValueColumnFetcher
+    , HibernateEntityWrapper hibernateEntityWrapper) {
+        this.namingStrategy = namingStrategy;
+        this.simpleValueBinder =simpleValueBinder;
+        this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+        this.manyToOneValuesBinder = manyToOneValuesBinder;
+        this.compositeIdentifierToManyToOneBinder = 
compositeIdentifierToManyToOneBinder;
+        this.simpleValueColumnFetcher = simpleValueColumnFetcher;
+        this.hibernateEntityWrapper = hibernateEntityWrapper;
+    }
+
+
+    /**
+     * Binds a many-to-one relationship to the
+     *
+     */
+    @SuppressWarnings("unchecked")
+    public void bindManyToOne(Association property
+            , ManyToOne manyToOne
+            ,String path) {
+        manyToOneValuesBinder.bindManyToOneValues(property, manyToOne);
+        PersistentEntity refDomainClass = property instanceof ManyToMany ? 
property.getOwner() : property.getAssociatedEntity();
+        Mapping mapping = hibernateEntityWrapper.getMappedForm(refDomainClass);
+
+        boolean isComposite = mapping.hasCompositeIdentifier();
+        if (isComposite) {
+            CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity();
+            
compositeIdentifierToManyToOneBinder.bindCompositeIdentifierToManyToOne(property,
 manyToOne, ci, refDomainClass, path);
+        }
+        else {
+            if (property.isCircular() && (property instanceof ManyToMany)) {
+                PropertyConfig pc = 
persistentPropertyToPropertyConfig.toPropertyConfig(property);
+
+                if (pc.getColumns().isEmpty()) {
+                    mapping.getColumns().put(property.getName(), pc);
+                }
+                if (!pc.hasJoinKeyMapping()) {
+                    JoinTable jt = new JoinTable();
+                    final ColumnConfig columnConfig = new ColumnConfig();
+                    
columnConfig.setName(namingStrategy.resolveColumnName(property.getName()) + 
FOREIGN_KEY_SUFFIX);
+                    jt.setKey(columnConfig);
+                    pc.setJoinTable(jt);
+                }
+                // set type
+                simpleValueBinder.bindSimpleValue(property, null, manyToOne, 
path);
+            }
+            else {
+                // bind column
+                // set type
+                simpleValueBinder.bindSimpleValue(property, null, manyToOne, 
path);
+            }
+        }
+
+        PropertyConfig config = 
persistentPropertyToPropertyConfig.toPropertyConfig(property);
+        if ((property instanceof 
org.grails.datastore.mapping.model.types.OneToOne) && !isComposite) {
+            manyToOne.setAlternateUniqueKey(true);
+            Column c = 
simpleValueColumnFetcher.getColumnForSimpleValue(manyToOne);
+            if (c == null) {
+                throw new MappingException("There is no column for property [" 
+ property.getName() + "]");
+            }
+            if (!config.isUniqueWithinGroup()) {
+                c.setUnique(config.isUnique());
+            }
+            else {
+                if (property.isBidirectional() && 
property.getInverseSide().isHasOne()) {
+                    c.setUnique(true);
+                }
+            }
+        }
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinder.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinder.java
new file mode 100644
index 0000000000..3d33be9ca2
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinder.java
@@ -0,0 +1,37 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import java.util.Optional;
+
+import org.hibernate.FetchMode;
+import org.hibernate.mapping.ManyToOne;
+
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+public class ManyToOneValuesBinder {
+    private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+
+    public ManyToOneValuesBinder() {
+        this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+    }
+
+    protected ManyToOneValuesBinder(PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig) {
+        this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+    }
+
+    public void bindManyToOneValues(Association property, ManyToOne manyToOne) 
{
+        PropertyConfig config = 
persistentPropertyToPropertyConfig.toPropertyConfig(property);
+
+        var fetchMode = 
Optional.ofNullable(config.getFetchMode()).orElse(FetchMode.DEFAULT);
+        manyToOne.setFetchMode(fetchMode);
+
+
+        var lazy = Optional.ofNullable(config.getLazy()).orElse(property != 
null);
+        manyToOne.setLazy(lazy);
+
+        manyToOne.setIgnoreNotFound(config.getIgnoreNotFound());
+
+        // set referenced entity
+        
manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName());
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueColumnFetcher.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueColumnFetcher.java
new file mode 100644
index 0000000000..9ccb35be26
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueColumnFetcher.java
@@ -0,0 +1,12 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import java.util.List;
+
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.SimpleValue;
+
+public class SimpleValueColumnFetcher {
+    public Column getColumnForSimpleValue(SimpleValue element) {
+        return element.getColumns().isEmpty() ? null : 
element.getColumns().iterator().next();
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinderSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinderSpec.groovy
new file mode 100644
index 0000000000..dffacf20f6
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneBinderSpec.groovy
@@ -0,0 +1,190 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.types.Association
+import org.grails.datastore.mapping.model.types.ManyToMany
+import org.grails.datastore.mapping.model.types.OneToOne
+import org.grails.orm.hibernate.cfg.CompositeIdentity
+import org.grails.orm.hibernate.cfg.JoinTable
+import org.grails.orm.hibernate.cfg.Mapping
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
+import org.grails.orm.hibernate.cfg.PropertyConfig
+import org.hibernate.MappingException
+import org.hibernate.mapping.Column
+import org.hibernate.mapping.ManyToOne
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class ManyToOneBinderSpec extends Specification {
+
+    @Unroll
+    def "Test bindManyToOne orchestration for #scenario"() {
+        given:
+        // 1. Create mocks for all dependencies
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def simpleValueBinder = Mock(SimpleValueBinder)
+        def propertyConfigConverter = Mock(PersistentPropertyToPropertyConfig)
+        def manyToOneValuesBinder = Mock(ManyToOneValuesBinder)
+        def compositeBinder = Mock(CompositeIdentifierToManyToOneBinder)
+        def columnFetcher = Mock(SimpleValueColumnFetcher)
+        def entityWrapper = Mock(HibernateEntityWrapper)
+
+        // 2. Instantiate the binder using the protected constructor
+        def binder = new ManyToOneBinder(namingStrategy, simpleValueBinder, 
propertyConfigConverter, manyToOneValuesBinder, compositeBinder, columnFetcher, 
entityWrapper)
+
+        // 3. Set up mocks for method arguments
+        def association = Mock(Association)
+        def manyToOne = Mock(ManyToOne)
+        def path = "/test"
+        def refDomainClass = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def propertyConfig = Mock(PropertyConfig)
+
+        // 4. Define mock behaviors
+        association.getAssociatedEntity() >> refDomainClass
+        entityWrapper.getMappedForm(refDomainClass) >> mapping
+        propertyConfigConverter.toPropertyConfig(association) >> propertyConfig
+        mapping.hasCompositeIdentifier() >> hasCompositeId
+
+        if (hasCompositeId) {
+            def compositeId = Mock(CompositeIdentity)
+            mapping.getIdentity() >> compositeId
+        }
+
+        when:
+        binder.bindManyToOne(association, manyToOne, path)
+
+        then:
+        // 5. Verify the orchestration logic
+        1 * manyToOneValuesBinder.bindManyToOneValues(association, manyToOne)
+        compositeBinderCalls * 
compositeBinder.bindCompositeIdentifierToManyToOne(association, manyToOne, _, 
refDomainClass, path)
+        simpleValueBinderCalls * 
simpleValueBinder.bindSimpleValue(association, null, manyToOne, path)
+
+        where:
+        scenario                 | hasCompositeId | compositeBinderCalls | 
simpleValueBinderCalls
+        "a composite identifier" | true           | 1                    | 0
+        "a simple identifier"    | false          | 0                    | 1
+    }
+
+    def "Test circular many-to-many binding"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def simpleValueBinder = Mock(SimpleValueBinder)
+        def propertyConfigConverter = Mock(PersistentPropertyToPropertyConfig)
+        def manyToOneValuesBinder = Mock(ManyToOneValuesBinder)
+        def compositeBinder = Mock(CompositeIdentifierToManyToOneBinder)
+        def columnFetcher = Mock(SimpleValueColumnFetcher)
+        def entityWrapper = Mock(HibernateEntityWrapper)
+
+        def binder = new ManyToOneBinder(namingStrategy, simpleValueBinder, 
propertyConfigConverter, manyToOneValuesBinder, compositeBinder, columnFetcher, 
entityWrapper)
+
+        def property = Mock(ManyToMany)
+        def manyToOne = Mock(ManyToOne)
+        def ownerEntity = Mock(PersistentEntity)
+        def mapping = new Mapping()
+        mapping.setColumns(new HashMap<String, PropertyConfig>())
+        def propertyConfig = new PropertyConfig()
+
+        property.isCircular() >> true
+        property.getOwner() >> ownerEntity
+        property.getName() >> "myCircularProp"
+        entityWrapper.getMappedForm(ownerEntity) >> mapping
+        propertyConfigConverter.toPropertyConfig(property) >> propertyConfig
+        namingStrategy.resolveColumnName("myCircularProp") >> 
"my_circular_prop"
+
+        when:
+        binder.bindManyToOne(property, manyToOne, "/test")
+
+        then:
+        1 * manyToOneValuesBinder.bindManyToOneValues(property, manyToOne)
+        1 * simpleValueBinder.bindSimpleValue(property, null, manyToOne, 
"/test")
+        def resultConfig = mapping.getColumns().get("myCircularProp")
+        resultConfig != null
+        resultConfig.getJoinTable().getKey().getName() == "my_circular_prop_id"
+    }
+
+    @Unroll
+    def "Test one-to-one binding with uniqueWithinGroup constraint for 
#scenario"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def simpleValueBinder = Mock(SimpleValueBinder)
+        def propertyConfigConverter = Mock(PersistentPropertyToPropertyConfig)
+        def manyToOneValuesBinder = Mock(ManyToOneValuesBinder)
+        def compositeBinder = Mock(CompositeIdentifierToManyToOneBinder)
+        def columnFetcher = Mock(SimpleValueColumnFetcher)
+        def entityWrapper = Mock(HibernateEntityWrapper)
+
+        def binder = new ManyToOneBinder(namingStrategy, simpleValueBinder, 
propertyConfigConverter, manyToOneValuesBinder, compositeBinder, columnFetcher, 
entityWrapper)
+
+        def property = Mock(OneToOne)
+        def manyToOne = Mock(ManyToOne)
+        def refDomainClass = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = Mock(Column)
+        def inverseSide = Mock(Association)
+
+        property.getAssociatedEntity() >> refDomainClass
+        entityWrapper.getMappedForm(refDomainClass) >> mapping
+        mapping.hasCompositeIdentifier() >> false
+        propertyConfigConverter.toPropertyConfig(property) >> propertyConfig
+        columnFetcher.getColumnForSimpleValue(manyToOne) >> column
+
+        // Configure mocks based on scenario
+        propertyConfig.isUnique() >> isUnique
+        propertyConfig.isUniqueWithinGroup() >> isUniqueWithinGroup
+        property.isBidirectional() >> isBidirectional
+        property.getInverseSide() >> inverseSide
+        inverseSide.isHasOne() >> isInverseHasOne
+
+        when:
+        binder.bindManyToOne(property, manyToOne, "/test")
+
+        then:
+        1 * manyToOne.setAlternateUniqueKey(true)
+        if (expectedUniqueValue != null) {
+            1 * column.setUnique(expectedUniqueValue)
+        } else {
+            0 * column.setUnique(_)
+        }
+
+        where:
+        scenario                               | isUnique | 
isUniqueWithinGroup | isBidirectional | isInverseHasOne | expectedUniqueValue
+        "simple unique=true"                   | true     | false              
 | false           | false           | true
+        "simple unique=false"                  | false    | false              
 | false           | false           | false
+        "uniqueWithinGroup and bidirectional"  | false    | true               
 | true            | true            | true
+        "uniqueWithinGroup and unidirectional" | false    | true               
 | false           | false           | null
+        "uniqueWithinGroup and not hasOne"     | false    | true               
 | true            | false           | null
+    }
+
+    def "Test one-to-one binding throws exception when column is not found"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def simpleValueBinder = Mock(SimpleValueBinder)
+        def propertyConfigConverter = Mock(PersistentPropertyToPropertyConfig)
+        def manyToOneValuesBinder = Mock(ManyToOneValuesBinder)
+        def compositeBinder = Mock(CompositeIdentifierToManyToOneBinder)
+        def columnFetcher = Mock(SimpleValueColumnFetcher)
+        def entityWrapper = Mock(HibernateEntityWrapper)
+
+        def binder = new ManyToOneBinder(namingStrategy, simpleValueBinder, 
propertyConfigConverter, manyToOneValuesBinder, compositeBinder, columnFetcher, 
entityWrapper)
+
+        def property = Mock(OneToOne)
+        def manyToOne = Mock(ManyToOne)
+        def refDomainClass = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def propertyConfig = new PropertyConfig()
+
+        property.getAssociatedEntity() >> refDomainClass
+        entityWrapper.getMappedForm(refDomainClass) >> mapping
+        mapping.hasCompositeIdentifier() >> false
+        propertyConfigConverter.toPropertyConfig(property) >> propertyConfig
+        columnFetcher.getColumnForSimpleValue(manyToOne) >> null // No column 
found
+
+        when:
+        binder.bindManyToOne(property, manyToOne, "/test")
+
+        then:
+        thrown(MappingException)
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinderSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinderSpec.groovy
new file mode 100644
index 0000000000..2dcf9d4810
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ManyToOneValuesBinderSpec.groovy
@@ -0,0 +1,54 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.types.Association
+import org.grails.orm.hibernate.cfg.PropertyConfig
+import org.hibernate.FetchMode
+import org.hibernate.mapping.ManyToOne
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class ManyToOneValuesBinderSpec extends Specification {
+
+    @Unroll
+    def "Test bindManyToOneValues with #scenario"() {
+        given:
+        // 1. Mock the dependency and use the protected constructor
+        def propertyConfigConverter = Mock(PersistentPropertyToPropertyConfig)
+        def binder = new ManyToOneValuesBinder(propertyConfigConverter)
+
+        // 2. Set up mocks for the method arguments
+        def association = Mock(Association)
+        def manyToOne = Mock(ManyToOne)
+        def associatedEntity = Mock(PersistentEntity)
+
+        // 3. Create the config object that the converter will return
+        def config = new PropertyConfig()
+        if (testFetchMode != null) {
+            config.setFetch(testFetchMode)
+        }
+        config.setLazy(testLazy)
+        config.setIgnoreNotFound(testIgnoreNotFound)
+
+        // 4. Define mock behaviors
+        propertyConfigConverter.toPropertyConfig(association) >> config
+        association.getAssociatedEntity() >> associatedEntity
+        associatedEntity.getName() >> "AssociatedEntityName"
+
+        when:
+        binder.bindManyToOneValues(association, manyToOne)
+
+        then:
+        // 5. Verify that the correct values were set on the ManyToOne object
+        1 * manyToOne.setFetchMode(expectedFetchMode)
+        1 * manyToOne.setLazy(expectedLazy)
+        1 * manyToOne.setIgnoreNotFound(testIgnoreNotFound)
+        1 * manyToOne.setReferencedEntityName("AssociatedEntityName")
+
+        where:
+        scenario                | testFetchMode    | testLazy | 
testIgnoreNotFound | expectedFetchMode | expectedLazy
+        "explicit values"       | FetchMode.JOIN   | true     | true           
    | FetchMode.JOIN    | true
+        "default values"        | null             | null     | false          
    | FetchMode.DEFAULT | true
+        "other explicit values" | FetchMode.SELECT | false    | false          
    | FetchMode.SELECT  | false
+    }
+}

Reply via email to