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