This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit f71eac0999a3d8bae37c830042979d776229077d Author: Walter Duque de Estrada <[email protected]> AuthorDate: Mon Mar 2 14:36:25 2026 -0600 refactor: CompositeIdentifierToManyToOneBinder --- .../CompositeIdentifierToManyToOneBinder.java | 196 ++++++++++----------- .../hibernate/GrailsHibernatePersistentEntity.java | 9 + ...CompositeIdentifierToManyToOneBinderSpec.groovy | 18 +- 3 files changed, 111 insertions(+), 112 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java index aa485a9740..785403f21d 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java @@ -20,130 +20,124 @@ package org.grails.orm.hibernate.cfg.domainbinding.binder; import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.UNDERSCORE; +import java.util.Arrays; import java.util.List; -import org.grails.datastore.mapping.model.PersistentEntity; -import org.grails.datastore.mapping.model.PersistentProperty; -import org.grails.datastore.mapping.model.types.ToOne; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + import org.grails.orm.hibernate.cfg.ColumnConfig; import org.grails.orm.hibernate.cfg.CompositeIdentity; import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToOneProperty; import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover; import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher; import org.grails.orm.hibernate.cfg.domainbinding.util.ForeignKeyColumnCountCalculator; + import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.mapping.SimpleValue; @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CompositeIdentifierToManyToOneBinder { - private final ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator; - private final PersistentEntityNamingStrategy namingStrategy; - private final DefaultColumnNameFetcher defaultColumnNameFetcher; - private final BackticksRemover backticksRemover; - private final SimpleValueBinder simpleValueBinder; - public CompositeIdentifierToManyToOneBinder( - ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator, - PersistentEntityNamingStrategy namingStrategy, - DefaultColumnNameFetcher defaultColumnNameFetcher, - BackticksRemover backticksRemover, - SimpleValueBinder simpleValueBinder) { - this.foreignKeyColumnCountCalculator = foreignKeyColumnCountCalculator; - this.namingStrategy = namingStrategy; - this.defaultColumnNameFetcher = defaultColumnNameFetcher; - this.backticksRemover = backticksRemover; - this.simpleValueBinder = simpleValueBinder; - } + private final ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator; + private final PersistentEntityNamingStrategy namingStrategy; + private final DefaultColumnNameFetcher defaultColumnNameFetcher; + private final BackticksRemover backticksRemover; + private final SimpleValueBinder simpleValueBinder; - public CompositeIdentifierToManyToOneBinder( - MetadataBuildingContext metadataBuildingContext, - PersistentEntityNamingStrategy namingStrategy, - JdbcEnvironment jdbcEnvironment) { - this( - new ForeignKeyColumnCountCalculator(), - namingStrategy, - new DefaultColumnNameFetcher(namingStrategy), - new BackticksRemover(), - new SimpleValueBinder(metadataBuildingContext, namingStrategy, jdbcEnvironment)); - } + public CompositeIdentifierToManyToOneBinder( + ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator, + PersistentEntityNamingStrategy namingStrategy, + DefaultColumnNameFetcher defaultColumnNameFetcher, + BackticksRemover backticksRemover, + SimpleValueBinder simpleValueBinder) { + this.foreignKeyColumnCountCalculator = foreignKeyColumnCountCalculator; + this.namingStrategy = namingStrategy; + this.defaultColumnNameFetcher = defaultColumnNameFetcher; + this.backticksRemover = backticksRemover; + this.simpleValueBinder = simpleValueBinder; + } - public void bindCompositeIdentifierToManyToOne( - HibernatePersistentProperty property, - SimpleValue value, - CompositeIdentity compositeId, - PersistentEntity refDomainClass, - String path) { - String[] propertyNames = compositeId.getPropertyNames(); + public CompositeIdentifierToManyToOneBinder( + MetadataBuildingContext metadataBuildingContext, + PersistentEntityNamingStrategy namingStrategy, + JdbcEnvironment jdbcEnvironment) { + this( + new ForeignKeyColumnCountCalculator(), + namingStrategy, + new DefaultColumnNameFetcher(namingStrategy), + new BackticksRemover(), + new SimpleValueBinder(metadataBuildingContext, namingStrategy, jdbcEnvironment)); + } - List<ColumnConfig> columns = property.getMappedForm().getColumns(); - int i = columns.size(); - int expectedForeignKeyColumnLength = - foreignKeyColumnCountCalculator.calculateForeignKeyColumnCount( - refDomainClass, propertyNames); - if (i != expectedForeignKeyColumnLength) { - int j = 0; - for (String propertyName : propertyNames) { - final ColumnConfig cc; - // if a column configuration exists in the mapping use it - if (j < i) { - cc = columns.get(j++); + public void bindCompositeIdentifierToManyToOne( + HibernatePersistentProperty property, + SimpleValue value, + CompositeIdentity compositeId, + GrailsHibernatePersistentEntity refDomainClass, + String path) { + String[] propertyNames = compositeId.getPropertyNames(); + List<ColumnConfig> columns = property.getMappedForm().getColumns(); + int existingCount = columns.size(); + if (existingCount != foreignKeyColumnCountCalculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames)) { + String prefix = refDomainClass.getTableName(namingStrategy); + IntStream.range(0, propertyNames.length) + .boxed() + .flatMap(idx -> { + ColumnConfig cc = idx < existingCount ? columns.get(idx) : new ColumnConfig(); + if (cc.getName() != null) { + return Stream.empty(); + } + String propertyName = propertyNames[idx]; + HibernatePersistentProperty ref = refDomainClass.getHibernatePropertyByName(propertyName); + return tryExpandNestedComposite(prefix, propertyName, ref) + .orElseGet(() -> singleColumn(prefix, propertyName, ref, cc)); + }) + .forEach(columns::add); } - // otherwise create a new one to represent the composite column - else { - cc = new ColumnConfig(); + simpleValueBinder.bindSimpleValue(property, null, value, path); + } + + /** + * If {@code ref} is a to-one whose associated entity has a composite identity, returns a stream + * of one named {@link ColumnConfig} per composite-identity property. Returns empty otherwise. + */ + private Optional<Stream<ColumnConfig>> tryExpandNestedComposite( + String prefix, String propertyName, HibernatePersistentProperty ref) { + if (!(ref instanceof HibernateToOneProperty toOne)) { + return Optional.empty(); } - // if the name is null then configure the name by convention - if (cc.getName() == null) { - // use the referenced table name as a prefix - String prefix = - refDomainClass instanceof GrailsHibernatePersistentEntity ghpe - ? ghpe.getTableName(namingStrategy) - : refDomainClass.getName(); - PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); + HibernatePersistentProperty[] nestedComposite = + toOne.getHibernateAssociatedEntity().getCompositeIdentity(); + if (nestedComposite == null) { + return Optional.empty(); + } + return Optional.of(Arrays.stream(nestedComposite) + .map(cip -> namedColumn(join(prefix, namingStrategy.resolveColumnName(propertyName), + defaultColumnNameFetcher.getDefaultColumnName(cip))))); + } - // if the referenced property is a ToOne and it has a composite id - // then a column is needed for each property that forms the composite id - if (referencedProperty instanceof ToOne toOne) { - PersistentProperty[] compositeIdentity = - toOne.getAssociatedEntity().getCompositeIdentity(); - if (compositeIdentity != null) { - for (PersistentProperty cip : compositeIdentity) { - // for each property of a composite id by default we use the table name and the - // property name as a prefix - String string = namingStrategy.resolveColumnName(referencedProperty.getName()); - String compositeIdPrefix = - backticksRemover.apply(prefix) + UNDERSCORE + backticksRemover.apply(string); + private Stream<ColumnConfig> singleColumn( + String prefix, String propertyName, HibernatePersistentProperty ref, ColumnConfig cc) { + String suffix = ref != null + ? defaultColumnNameFetcher.getDefaultColumnName(ref) + : propertyName; + cc.setName(join(prefix, suffix)); + return Stream.of(cc); + } - String suffix = - cip instanceof HibernatePersistentProperty ghpp - ? defaultColumnNameFetcher.getDefaultColumnName(ghpp) - : cip.getName(); - String finalColumnName = - backticksRemover.apply(compositeIdPrefix) - + UNDERSCORE - + backticksRemover.apply(suffix); - ColumnConfig newCc = new ColumnConfig(); - newCc.setName(finalColumnName); - columns.add(newCc); - } - continue; - } - } + private ColumnConfig namedColumn(String name) { + ColumnConfig cc = new ColumnConfig(); + cc.setName(name); + return cc; + } - String suffix = - referencedProperty instanceof HibernatePersistentProperty ghpp - ? defaultColumnNameFetcher.getDefaultColumnName(ghpp) - : referencedProperty.getName(); - String finalColumnName = - backticksRemover.apply(prefix) + UNDERSCORE + backticksRemover.apply(suffix); - cc.setName(finalColumnName); - columns.add(cc); - } - } + private String join(String... parts) { + return Arrays.stream(parts).map(backticksRemover::apply).collect(Collectors.joining(String.valueOf(UNDERSCORE))); } - // set type - simpleValueBinder.bindSimpleValue(property, null, value, path); - } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java index 20ac8e2c7b..0337ab1acb 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java @@ -156,6 +156,15 @@ public interface GrailsHibernatePersistentEntity extends PersistentEntity { @Override HibernatePersistentProperty getVersion(); + /** + * Returns the persistent property with the given name cast to {@link HibernatePersistentProperty}, + * or {@code null} if no such property exists. + */ + default HibernatePersistentProperty getHibernatePropertyByName(String name) { + var property = getPropertyByName(name); + return property instanceof HibernatePersistentProperty hpp ? hpp : null; + } + /** * @param parentType The type of the parent entity * @return The parent property if it exists diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy index ca337b414b..71cc4ed150 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy @@ -1,8 +1,7 @@ package org.grails.orm.hibernate.cfg.domainbinding -import org.grails.datastore.mapping.model.PersistentProperty -import org.grails.datastore.mapping.model.types.ToOne +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToOneProperty import org.grails.orm.hibernate.cfg.ColumnConfig import org.grails.orm.hibernate.cfg.CompositeIdentity import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity @@ -34,8 +33,7 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { def binder = new CompositeIdentifierToManyToOneBinder(calculator, namingStrategy, columnNameFetcher, backticksRemover, simpleValueBinder) // 2. Set up stubs for the method arguments - def association = Mock(ToOne) - association.asType(HibernatePersistentProperty) >> association + def association = Mock(HibernatePersistentProperty) def value = Mock(SimpleValue) def refDomainClass = Mock(GrailsHibernatePersistentEntity) def path = "/test" @@ -51,17 +49,16 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { calculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames) >> 2 - def nestedEntityProp = Mock(ToOne) - nestedEntityProp.asType(HibernatePersistentProperty) >> nestedEntityProp - refDomainClass.getPropertyByName("nestedEntity") >> nestedEntityProp + def nestedEntityProp = Mock(HibernateToOneProperty) + refDomainClass.getHibernatePropertyByName("nestedEntity") >> nestedEntityProp nestedEntityProp.name >> "nestedEntity" def nestedAssociatedEntity = Mock(GrailsHibernatePersistentEntity) - nestedEntityProp.getAssociatedEntity() >> nestedAssociatedEntity + nestedEntityProp.getHibernateAssociatedEntity() >> nestedAssociatedEntity def nestedPartA = Mock(HibernatePersistentProperty) def nestedPartB = Mock(HibernatePersistentProperty) - def perArray = [nestedPartA, nestedPartB] as PersistentProperty[] + def perArray = [nestedPartA, nestedPartB] as HibernatePersistentProperty[] nestedAssociatedEntity.getCompositeIdentity() >> perArray // 4. Mock the behavior of the dependency methods @@ -100,8 +97,7 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { def binder = new CompositeIdentifierToManyToOneBinder(calculator, namingStrategy, columnNameFetcher, backticksRemover, simpleValueBinder) // 2. Set up arguments - def association = Mock(ToOne) - association.asType(HibernatePersistentProperty) >> association + def association = Mock(HibernatePersistentProperty) def value = Mock(SimpleValue) def compositeId = new CompositeIdentity() compositeId.setPropertyNames(["prop1", "prop2"] as String[])
