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