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 f76b61e8027a62c8278df3ddfe1d9dfeb602624f
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Wed Feb 18 15:16:37 2026 -0600

    Refactor unidirectional one-to-many binding into 
UnidirectionalOneToManyBinder and add spec
---
 .../cfg/domainbinding/binder/CollectionBinder.java |  23 ++--
 .../secondpass/CollectionSecondPassBinder.java     |  43 ++-----
 .../secondpass/UnidirectionalOneToManyBinder.java  |  69 +++++++++++
 .../UnidirectionalOneToManyBinderSpec.groovy       | 133 +++++++++++++++++++++
 4 files changed, 224 insertions(+), 44 deletions(-)

diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java
index aa045eb6e0..67fbf427c6 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java
@@ -32,6 +32,7 @@ import 
org.grails.orm.hibernate.cfg.domainbinding.util.GrailsPropertyResolver;
 
 import 
org.grails.orm.hibernate.cfg.domainbinding.secondpass.CollectionWithJoinTableBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.secondpass.UnidirectionalOneToManyInverseValuesBinder;
+import 
org.grails.orm.hibernate.cfg.domainbinding.secondpass.UnidirectionalOneToManyBinder;
 import org.hibernate.FetchMode;
 import org.hibernate.boot.spi.InFlightMetadataCollector;
 import org.hibernate.boot.spi.MetadataBuildingContext;
@@ -56,6 +57,7 @@ public class CollectionBinder {
     private final ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher;
     private final ListSecondPassBinder listSecondPassBinder;
     private final CollectionSecondPassBinder collectionSecondPassBinder;
+    private final UnidirectionalOneToManyBinder unidirectionalOneToManyBinder;
     private final MapSecondPassBinder mapSecondPassBinder;
 
     public CollectionBinder(
@@ -76,6 +78,16 @@ public class CollectionBinder {
         GrailsPropertyResolver grailsPropertyResolver = new 
GrailsPropertyResolver();
         CollectionForPropertyConfigBinder collectionForPropertyConfigBinder = 
new CollectionForPropertyConfigBinder();
         UnidirectionalOneToManyInverseValuesBinder 
unidirectionalOneToManyInverseValuesBinder = new 
UnidirectionalOneToManyInverseValuesBinder();
+        CollectionWithJoinTableBinder collectionWithJoinTableBinder = new 
CollectionWithJoinTableBinder(
+                metadataBuildingContext,
+                namingStrategy,
+                unidirectionalOneToManyInverseValuesBinder,
+                enumTypeBinder,
+                compositeIdentifierToManyToOneBinder,
+                simpleValueColumnFetcher,
+                collectionForPropertyConfigBinder
+        );
+        this.unidirectionalOneToManyBinder = new 
UnidirectionalOneToManyBinder(collectionWithJoinTableBinder);
         this.collectionSecondPassBinder = new CollectionSecondPassBinder(
                 metadataBuildingContext,
                 namingStrategy,
@@ -91,15 +103,8 @@ public class CollectionBinder {
                 new BidirectionalOneToManyLinker(grailsPropertyResolver),
                 new DependentKeyValueBinder(simpleValueBinder, 
compositeIdentifierToManyToOneBinder),
                 unidirectionalOneToManyInverseValuesBinder,
-                new CollectionWithJoinTableBinder(
-                        metadataBuildingContext,
-                        namingStrategy,
-                        unidirectionalOneToManyInverseValuesBinder,
-                        enumTypeBinder,
-                        compositeIdentifierToManyToOneBinder,
-                        simpleValueColumnFetcher,
-                        collectionForPropertyConfigBinder
-                )
+                unidirectionalOneToManyBinder,
+                collectionWithJoinTableBinder
         );
         this.listSecondPassBinder = new 
ListSecondPassBinder(metadataBuildingContext, namingStrategy, 
collectionSecondPassBinder);
         this.mapSecondPassBinder = new 
MapSecondPassBinder(metadataBuildingContext, namingStrategy, 
collectionSecondPassBinder);
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
index c326cd5690..fb4a94c0bf 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
@@ -63,6 +63,7 @@ public class CollectionSecondPassBinder {
     private final BidirectionalOneToManyLinker bidirectionalOneToManyLinker;
     private final DependentKeyValueBinder dependentKeyValueBinder;
     private final UnidirectionalOneToManyInverseValuesBinder 
unidirectionalOneToManyInverseValuesBinder;
+    private final UnidirectionalOneToManyBinder unidirectionalOneToManyBinder;
     private final CollectionWithJoinTableBinder collectionWithJoinTableBinder;
 
     public CollectionSecondPassBinder(
@@ -80,6 +81,7 @@ public class CollectionSecondPassBinder {
             BidirectionalOneToManyLinker bidirectionalOneToManyLinker,
             DependentKeyValueBinder dependentKeyValueBinder,
             UnidirectionalOneToManyInverseValuesBinder 
unidirectionalOneToManyInverseValuesBinder,
+            UnidirectionalOneToManyBinder unidirectionalOneToManyBinder,
             CollectionWithJoinTableBinder collectionWithJoinTableBinder) {
         this.metadataBuildingContext = metadataBuildingContext;
         this.namingStrategy = namingStrategy;
@@ -95,6 +97,7 @@ public class CollectionSecondPassBinder {
         this.bidirectionalOneToManyLinker = bidirectionalOneToManyLinker;
         this.dependentKeyValueBinder = dependentKeyValueBinder;
         this.unidirectionalOneToManyInverseValuesBinder = 
unidirectionalOneToManyInverseValuesBinder;
+        this.unidirectionalOneToManyBinder = unidirectionalOneToManyBinder;
         this.collectionWithJoinTableBinder = collectionWithJoinTableBinder;
         this.defaultColumnNameFetcher = new 
DefaultColumnNameFetcher(namingStrategy);
         this.orderByClauseBuilder = new OrderByClauseBuilder();
@@ -102,8 +105,10 @@ public class CollectionSecondPassBinder {
 
 
 
-    public void bindCollectionSecondPass(@Nonnull HibernateToManyProperty 
property, @Nonnull InFlightMetadataCollector mappings,
-                                         Map<?, ?> persistentClasses, @Nonnull 
Collection collection) {
+    public void bindCollectionSecondPass(@Nonnull HibernateToManyProperty 
property,
+                                         @Nonnull InFlightMetadataCollector 
mappings,
+                                         Map<?, ?> persistentClasses,
+                                         @Nonnull Collection collection) {
         PersistentClass associatedClass = null;
 
         if (LOG.isDebugEnabled())
@@ -238,46 +243,14 @@ public class CollectionSecondPassBinder {
                 // TODO support unidirectional many-to-many
             }
         } else if (property.isUnidirectionalOneToMany()) {
-            // for non-inverse one-to-many, with a not-null fk, add a backref!
-            // there are problems with list and map mappings and join columns 
relating to duplicate key constraints
-            // TODO change this when HHH-1268 is resolved
-            if (!property.shouldBindWithForeignKey()) {
-                
collectionWithJoinTableBinder.bindCollectionWithJoinTable(property, mappings, 
collection);
-            } else {
-                bindUnidirectionalOneToMany((HibernateOneToManyProperty) 
property, mappings, collection);
-            }
+            unidirectionalOneToManyBinder.bind((HibernateOneToManyProperty) 
property, mappings, collection);
         } else if (property.supportsJoinColumnMapping()) {
             
collectionWithJoinTableBinder.bindCollectionWithJoinTable(property, mappings, 
collection);
         }
         collectionKeyColumnUpdater.forceNullableAndCheckUpdatable(key, 
property); // Use the injected service
     }
 
-    private void bindUnidirectionalOneToMany(HibernateOneToManyProperty 
property, @Nonnull InFlightMetadataCollector mappings, Collection collection) {
-        Value v = collection.getElement();
-        v.createForeignKey();
-        String entityName;
-        if (v instanceof ManyToOne) {
-            ManyToOne manyToOne = (ManyToOne) v;
 
-            entityName = manyToOne.getReferencedEntityName();
-        } else {
-            entityName = ((OneToMany) v).getReferencedEntityName();
-        }
-        collection.setInverse(false);
-        PersistentClass referenced = mappings.getEntityBinding(entityName);
-        Backref prop = new Backref();
-        GrailsHibernatePersistentEntity owner = 
(GrailsHibernatePersistentEntity) property.getOwner();
-        prop.setEntityName(owner.getName());
-        String s2 = property.getName();
-        prop.setName(UNDERSCORE + new 
BackticksRemover().apply(owner.getJavaClass().getSimpleName()) + UNDERSCORE + 
new BackticksRemover().apply(s2) + "Backref");
-        prop.setUpdatable(false);
-        prop.setInsertable(true);
-        prop.setCollectionRole(collection.getRole());
-        prop.setValue(collection.getKey());
-        prop.setOptional(true);
-
-        referenced.addProperty(prop);
-    }
 
 
 
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinder.java
new file mode 100644
index 0000000000..d4b9904d4e
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinder.java
@@ -0,0 +1,69 @@
+package org.grails.orm.hibernate.cfg.domainbinding.secondpass;
+
+import jakarta.annotation.Nonnull;
+import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity;
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty;
+import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover;
+import org.hibernate.boot.spi.InFlightMetadataCollector;
+import org.hibernate.mapping.Backref;
+import org.hibernate.mapping.Collection;
+import org.hibernate.mapping.ManyToOne;
+import org.hibernate.mapping.OneToMany;
+import org.hibernate.mapping.PersistentClass;
+import org.hibernate.mapping.Value;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.UNDERSCORE;
+
+/**
+ * Binds unidirectional one-to-many associations.
+ */
+public class UnidirectionalOneToManyBinder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(UnidirectionalOneToManyBinder.class);
+    private final CollectionWithJoinTableBinder collectionWithJoinTableBinder;
+    private final BackticksRemover backticksRemover = new BackticksRemover();
+
+    public UnidirectionalOneToManyBinder(CollectionWithJoinTableBinder 
collectionWithJoinTableBinder) {
+        this.collectionWithJoinTableBinder = collectionWithJoinTableBinder;
+    }
+
+    public void bind(@Nonnull HibernateOneToManyProperty property,
+                     @Nonnull InFlightMetadataCollector mappings,
+                     @Nonnull Collection collection) {
+        if (!property.shouldBindWithForeignKey()) {
+            
collectionWithJoinTableBinder.bindCollectionWithJoinTable(property, mappings, 
collection);
+        } else {
+            bindUnidirectionalOneToMany(property, mappings, collection);
+        }
+    }
+
+    private void bindUnidirectionalOneToMany(@Nonnull 
HibernateOneToManyProperty property,
+                                              @Nonnull 
InFlightMetadataCollector mappings,
+                                              @Nonnull Collection collection) {
+        Value element = collection.getElement();
+        element.createForeignKey();
+
+        String entityName = (element instanceof ManyToOne manyToOne)
+                ? manyToOne.getReferencedEntityName()
+                : ((OneToMany) element).getReferencedEntityName();
+
+        collection.setInverse(false);
+
+        
mappings.getEntityBinding(entityName).addProperty(createBackref(property, 
collection));
+    }
+
+    private Backref createBackref(HibernateOneToManyProperty property, 
Collection collection) {
+        GrailsHibernatePersistentEntity owner = 
(GrailsHibernatePersistentEntity) property.getOwner();
+        Backref backref = new Backref();
+        backref.setEntityName(owner.getName());
+        backref.setName(UNDERSCORE + 
backticksRemover.apply(owner.getJavaClass().getSimpleName()) + UNDERSCORE + 
backticksRemover.apply(property.getName()) + "Backref");
+        backref.setUpdatable(false);
+        backref.setInsertable(true);
+        backref.setCollectionRole(collection.getRole());
+        backref.setValue(collection.getKey());
+        backref.setOptional(true);
+        return backref;
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinderSpec.groovy
new file mode 100644
index 0000000000..27e9fb780f
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/UnidirectionalOneToManyBinderSpec.groovy
@@ -0,0 +1,133 @@
+package org.grails.orm.hibernate.cfg.domainbinding.secondpass;
+
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.persistence.Entity
+import org.grails.orm.hibernate.cfg.GrailsDomainBinder
+import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity
+import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionForPropertyConfigBinder
+import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder
+import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty
+import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover
+import 
org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher
+import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher
+import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher
+import org.hibernate.mapping.Backref
+import org.hibernate.mapping.Bag
+import org.hibernate.mapping.BasicValue
+import org.hibernate.mapping.OneToMany
+import org.hibernate.mapping.PersistentClass
+import spock.lang.Subject
+
+class UnidirectionalOneToManyBinderSpec extends HibernateGormDatastoreSpec {
+
+    @Subject
+    UnidirectionalOneToManyBinder binder
+
+    def setupSpec() {
+        manager.addAllDomainClasses([
+                UniOwner, UniPet
+        ])
+    }
+
+    def setup() {
+        def grailsDomainBinder = getGrailsDomainBinder()
+        def metadataBuildingContext = 
grailsDomainBinder.getMetadataBuildingContext()
+        def namingStrategy = grailsDomainBinder.getNamingStrategy()
+        def jdbcEnvironment = grailsDomainBinder.getJdbcEnvironment()
+        def defaultColumnNameFetcher = new 
DefaultColumnNameFetcher(namingStrategy)
+        def backticksRemover = new BackticksRemover()
+        def columnNameForPropertyAndPathFetcher = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy, defaultColumnNameFetcher, 
backticksRemover)
+
+        def unidirectionalOneToManyInverseValuesBinder = new 
UnidirectionalOneToManyInverseValuesBinder()
+        def enumTypeBinder = new EnumTypeBinder(metadataBuildingContext, 
columnNameForPropertyAndPathFetcher)
+        def compositeIdentifierToManyToOneBinder = new 
CompositeIdentifierToManyToOneBinder(metadataBuildingContext, namingStrategy, 
jdbcEnvironment)
+        def simpleValueColumnFetcher = new SimpleValueColumnFetcher()
+        def collectionForPropertyConfigBinder = new 
CollectionForPropertyConfigBinder()
+
+        def collectionWithJoinTableBinder = new CollectionWithJoinTableBinder(
+                metadataBuildingContext,
+                namingStrategy,
+                unidirectionalOneToManyInverseValuesBinder,
+                enumTypeBinder,
+                compositeIdentifierToManyToOneBinder,
+                simpleValueColumnFetcher,
+                collectionForPropertyConfigBinder
+        )
+        binder = new 
UnidirectionalOneToManyBinder(collectionWithJoinTableBinder)
+    }
+
+    def "test bindUnidirectionalOneToMany with join table"() {
+        given:
+        def grailsDomainBinder = getGrailsDomainBinder()
+        def ownerEntity = 
grailsDomainBinder.hibernateMappingContext.getPersistentEntity(UniOwner.name) 
as GrailsHibernatePersistentEntity
+        def petEntity = 
grailsDomainBinder.hibernateMappingContext.getPersistentEntity(UniPet.name) as 
GrailsHibernatePersistentEntity
+
+        def ownerToPetsProperty = ownerEntity.getPropertyByName("pets") as 
HibernateOneToManyProperty
+
+        def mappings = 
grailsDomainBinder.metadataBuildingContext.metadataCollector
+        def ownerPersistentClass = mappings.getEntityBinding(UniOwner.name)
+        def collection = new Bag(grailsDomainBinder.metadataBuildingContext, 
ownerPersistentClass)
+        def role = UniOwner.name + ".pets"
+        collection.setRole(role)
+        collection.setCollectionTable(ownerPersistentClass.getTable()) // Just 
use owner table for simplicity in this test
+        def element = new 
OneToMany(grailsDomainBinder.metadataBuildingContext, ownerPersistentClass)
+        element.setReferencedEntityName(petEntity.getName())
+        collection.setElement(element)
+        collection.setKey(new 
BasicValue(grailsDomainBinder.metadataBuildingContext, 
ownerPersistentClass.getTable()))
+
+        when:
+        binder.bind(ownerToPetsProperty, mappings, collection)
+
+        then:
+        collection.isInverse() == false
+        // By default it uses join table because shouldBindWithForeignKey() is 
false for unidirectional OTM in hibernate7
+        collection.getElement() instanceof org.hibernate.mapping.ManyToOne 
+    }
+
+    def "test bindUnidirectionalOneToMany with backref"() {
+        given:
+        def grailsDomainBinder = getGrailsDomainBinder()
+        def ownerEntity = 
grailsDomainBinder.hibernateMappingContext.getPersistentEntity(UniOwner.name) 
as GrailsHibernatePersistentEntity
+        def petEntity = 
grailsDomainBinder.hibernateMappingContext.getPersistentEntity(UniPet.name) as 
GrailsHibernatePersistentEntity
+
+        // Use a Stub for the property to override shouldBindWithForeignKey
+        def ownerToPetsProperty = Stub(HibernateOneToManyProperty) {
+            shouldBindWithForeignKey() >> true
+            getOwner() >> ownerEntity
+            getName() >> "pets"
+        }
+
+        def mappings = 
grailsDomainBinder.metadataBuildingContext.metadataCollector
+        def ownerPersistentClass = mappings.getEntityBinding(UniOwner.name)
+        def petPersistentClass = mappings.getEntityBinding(UniPet.name)
+
+        def collection = new Bag(grailsDomainBinder.metadataBuildingContext, 
ownerPersistentClass)
+        collection.setRole(UniOwner.name + ".pets")
+
+        def element = new 
OneToMany(grailsDomainBinder.metadataBuildingContext, ownerPersistentClass)
+        element.setReferencedEntityName(petEntity.getName())
+        collection.setElement(element)
+        collection.setKey(new 
BasicValue(grailsDomainBinder.metadataBuildingContext, 
ownerPersistentClass.getTable()))
+
+        when:
+        binder.bind(ownerToPetsProperty, mappings, collection)
+
+        then:
+        collection.isInverse() == false
+        petPersistentClass.getProperty("_UniOwner_petsBackref") != null
+    }
+
+}
+
+@Entity
+class UniOwner {
+    Long id
+    Set<UniPet> pets
+    static hasMany = [pets: UniPet]
+}
+
+@Entity
+class UniPet {
+    Long id
+}

Reply via email to