You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2025/10/11 17:17:50 UTC
(grails-core) 05/10: CompositeIdentifierToManyToOneBinder
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 1123f19b9275962a714c9449a20132ce3050787a
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Tue Sep 30 21:02:22 2025 -0500
CompositeIdentifierToManyToOneBinder
---
.../orm/hibernate/cfg/GrailsDomainBinder.java | 68 +----------
.../CompositeIdentifierToManyToOneBinder.java | 111 ++++++++++++++++++
.../PersistentPropertyToPropertyConfig.java | 4 +-
...CompositeIdentifierToManyToOneBinderSpec.groovy | 124 +++++++++++++++++++++
4 files changed, 241 insertions(+), 66 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 d8df2b6449..624c407351 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
@@ -28,7 +28,6 @@ import org.grails.datastore.mapping.model.types.Embedded;
import org.grails.datastore.mapping.model.types.ManyToMany;
import org.grails.datastore.mapping.model.types.TenantId;
import org.grails.datastore.mapping.model.types.ToMany;
-import org.grails.datastore.mapping.model.types.ToOne;
import org.grails.orm.hibernate.cfg.domainbinding.ClassBinder;
import org.grails.orm.hibernate.cfg.domainbinding.ColumnConfigToColumnBinder;
import org.grails.orm.hibernate.cfg.domainbinding.ConfigureDerivedPropertiesConsumer;
@@ -778,8 +777,7 @@ public class GrailsDomainBinder implements MetadataContributor {
Mapping m = new HibernateEntityWrapper().getMappedForm(domainClass);
if (m.hasCompositeIdentifier()) {
CompositeIdentity ci = (CompositeIdentity) m.getIdentity();
- bindCompositeIdentifierToManyToOne(property, element, ci, domainClass,
- EMPTY_PATH, sessionFactoryBeanName);
+ new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne(property, element, ci, domainClass, EMPTY_PATH);
}
else {
if (joinColumnMappingOptional.isPresent()) {
@@ -856,7 +854,7 @@ public class GrailsDomainBinder implements MetadataContributor {
if ((shouldCollectionBindWithJoinColumn((ToMany) property) && hasCompositeIdentifier) ||
(hasCompositeIdentifier && ( property instanceof ManyToMany))) {
CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity();
- bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH, sessionFactoryBeanName);
+ new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH);
}
else {
// set type
@@ -2054,7 +2052,7 @@ public class GrailsDomainBinder implements MetadataContributor {
boolean isComposite = mapping.hasCompositeIdentifier();
if (isComposite) {
CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity();
- bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path, sessionFactoryBeanName);
+ new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path);
}
else {
//TODO NOT TESTED
@@ -2097,66 +2095,6 @@ public class GrailsDomainBinder implements MetadataContributor {
}
}
- private void bindCompositeIdentifierToManyToOne(Association property,
- SimpleValue value, CompositeIdentity compositeId, PersistentEntity refDomainClass,
- String path, String sessionFactoryBeanName) {
- String[] propertyNames = compositeId.getPropertyNames();
- PropertyConfig config = new PersistentPropertyToPropertyConfig().apply(property);
-
- List<ColumnConfig> columns = config.getColumns();
- int i = columns.size();
- int expectedForeignKeyColumnLength = new ForeignKeyColumnCountCalculator().calculateForeignKeyColumnCount(refDomainClass, propertyNames);
- if (i != expectedForeignKeyColumnLength) {
- int j = 0;
- for (String propertyName : propertyNames) {
- ColumnConfig cc;
- // if a column configuration exists in the mapping use it
- if(j < i) {
- cc = columns.get(j++);
- }
- // otherwise create a new one to represent the composite column
- else {
- cc = new ColumnConfig();
- }
- // 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 = new TableNameFetcher(getNamingStrategy()).getTableName(refDomainClass);
- PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName);
-
- // 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 toOne = (ToOne) referencedProperty;
- 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 = getNamingStrategy().resolveColumnName(referencedProperty.getName());
- String compositeIdPrefix = new BackticksRemover().apply(prefix) + UNDERSCORE + new BackticksRemover().apply(string);
-
- String suffix = new DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(cip);
- String finalColumnName = new BackticksRemover().apply(compositeIdPrefix) + UNDERSCORE + new BackticksRemover().apply(suffix);
- cc = new ColumnConfig();
- cc.setName(finalColumnName);
- columns.add(cc);
- }
- continue;
- }
- }
-
- String suffix = new DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(referencedProperty);
- String finalColumnName = new BackticksRemover().apply(prefix) + UNDERSCORE + new BackticksRemover().apply(suffix);
- cc.setName(finalColumnName);
- columns.add(cc);
- }
- }
- }
- new PersistentPropertyToPropertyConfig().apply(property);
- // set type
- new SimpleValueBinder(namingStrategy).bindSimpleValue(property, null, value, path);
- }
-
// 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
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java
new file mode 100644
index 0000000000..d572808661
--- /dev/null
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinder.java
@@ -0,0 +1,111 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import java.util.List;
+
+import org.hibernate.mapping.SimpleValue;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.model.types.ToOne;
+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.PropertyConfig;
+
+import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.UNDERSCORE;
+
+public class CompositeIdentifierToManyToOneBinder {
+ private final ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator;
+ private final TableNameFetcher tableNameFetcher;
+ private final PersistentEntityNamingStrategy namingStrategy;
+ private final DefaultColumnNameFetcher defaultColumnNameFetcher;
+ private final PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig;
+ private final BackticksRemover backticksRemover;
+ private final SimpleValueBinder simpleValueBinder;
+
+ public CompositeIdentifierToManyToOneBinder(PersistentEntityNamingStrategy namingStrategy){
+ this.foreignKeyColumnCountCalculator = new ForeignKeyColumnCountCalculator();
+ this.tableNameFetcher = new TableNameFetcher(namingStrategy);
+ this.namingStrategy = namingStrategy;
+ this.defaultColumnNameFetcher = new DefaultColumnNameFetcher(namingStrategy);
+ this.persistentPropertyToPropertyConfig = new PersistentPropertyToPropertyConfig();
+ this.backticksRemover = new BackticksRemover();
+ this.simpleValueBinder = new SimpleValueBinder(namingStrategy);
+
+ }
+
+ protected CompositeIdentifierToManyToOneBinder(ForeignKeyColumnCountCalculator foreignKeyColumnCountCalculator
+ , TableNameFetcher tableNameFetcher
+ , PersistentEntityNamingStrategy namingStrategy
+ , DefaultColumnNameFetcher defaultColumnNameFetcher
+ , PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig
+ , BackticksRemover backticksRemover, SimpleValueBinder simpleValueBinder) {
+ this.foreignKeyColumnCountCalculator = foreignKeyColumnCountCalculator;
+ this.tableNameFetcher =tableNameFetcher;
+ this.namingStrategy = namingStrategy;
+ this.defaultColumnNameFetcher = defaultColumnNameFetcher;
+ this.persistentPropertyToPropertyConfig = persistentPropertyToPropertyConfig;
+ this.backticksRemover = backticksRemover;
+ this.simpleValueBinder = simpleValueBinder;
+ }
+
+ public void bindCompositeIdentifierToManyToOne(Association property,
+ SimpleValue value,
+ CompositeIdentity compositeId,
+ PersistentEntity refDomainClass,
+ String path) {
+ String[] propertyNames = compositeId.getPropertyNames();
+
+ List<ColumnConfig> columns = persistentPropertyToPropertyConfig.apply(property).getColumns();
+ int i = columns.size();
+ int expectedForeignKeyColumnLength = foreignKeyColumnCountCalculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames);
+ if (i != expectedForeignKeyColumnLength) {
+ int j = 0;
+ for (String propertyName : propertyNames) {
+ ColumnConfig cc;
+ // if a column configuration exists in the mapping use it
+ if (j < i) {
+ cc = columns.get(j++);
+ }
+ // otherwise create a new one to represent the composite column
+ else {
+ cc = new ColumnConfig();
+ }
+ // 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 = tableNameFetcher.getTableName(refDomainClass);
+ PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName);
+
+ // 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);
+
+ String suffix = defaultColumnNameFetcher.getDefaultColumnName(cip);
+ String finalColumnName = backticksRemover.apply(compositeIdPrefix) + UNDERSCORE + backticksRemover.apply(suffix);
+ cc = new ColumnConfig();
+ cc.setName(finalColumnName);
+ columns.add(cc);
+ }
+ continue;
+ }
+ }
+
+ String suffix = defaultColumnNameFetcher.getDefaultColumnName(referencedProperty);
+ String finalColumnName = backticksRemover.apply(prefix) + UNDERSCORE + backticksRemover.apply(suffix);
+ cc.setName(finalColumnName);
+ columns.add(cc);
+ }
+ }
+ }
+ // set type
+ simpleValueBinder.bindSimpleValue(property, null, value, path);
+ }
+}
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java
index 53be315bf3..3272159ae8 100644
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/PersistentPropertyToPropertyConfig.java
@@ -7,9 +7,11 @@ import org.hibernate.MappingException;
import java.util.Optional;
import java.util.function.Function;
+import jakarta.annotation.Nonnull;
+
public class PersistentPropertyToPropertyConfig implements Function<PersistentProperty, PropertyConfig> {
@Override
- public PropertyConfig apply(PersistentProperty persistentProperty) {
+ @Nonnull public PropertyConfig apply(PersistentProperty persistentProperty) {
return Optional.ofNullable(persistentProperty)
.map(PersistentProperty::getMappedForm)
.map(PropertyConfig.class::cast)
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy
new file mode 100644
index 0000000000..61bfa0d732
--- /dev/null
+++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy
@@ -0,0 +1,124 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.ToOne
+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.PropertyConfig
+import org.hibernate.mapping.SimpleValue
+import spock.lang.Specification
+
+class CompositeIdentifierToManyToOneBinderSpec extends Specification {
+
+ def "Test bindCompositeIdentifierToManyToOne with nested composite ID"() {
+ given:
+ // 1. Stub all dependencies for the protected constructor
+ def calculator = Stub(ForeignKeyColumnCountCalculator)
+ def tableNameFetcher = Stub(TableNameFetcher)
+ def namingStrategy = Stub(PersistentEntityNamingStrategy)
+ def columnNameFetcher = Stub(DefaultColumnNameFetcher)
+ def propertyConfigConverter = Stub(PersistentPropertyToPropertyConfig)
+ def backticksRemover = Stub(BackticksRemover)
+ def simpleValueBinder = Mock(SimpleValueBinder) // Use Mock to verify interaction
+
+ // Instantiate the binder with stubs
+ def binder = new CompositeIdentifierToManyToOneBinder(calculator, tableNameFetcher, namingStrategy, columnNameFetcher, propertyConfigConverter, backticksRemover, simpleValueBinder)
+
+ // 2. Set up stubs for the method arguments
+ def association = Stub(ToOne)
+ def value = Stub(SimpleValue)
+ def refDomainClass = Stub(PersistentEntity)
+ def path = "/test"
+
+ // Use a real CompositeIdentity object to avoid final method mocking issues
+ def propertyNames = ["nestedEntity"] as String[]
+ def compositeId = new CompositeIdentity()
+ compositeId.setPropertyNames(propertyNames)
+
+ // 3. Define the nested composite key scenario
+ def propertyConfig = new PropertyConfig()
+ propertyConfigConverter.apply(association) >> propertyConfig
+
+ calculator.calculateForeignKeyColumnCount(refDomainClass, propertyNames) >> 2
+
+ def nestedEntityProp = Stub(ToOne)
+ refDomainClass.getPropertyByName("nestedEntity") >> nestedEntityProp
+ nestedEntityProp.name >> "nestedEntity"
+
+ def nestedAssociatedEntity = Stub(PersistentEntity)
+ nestedEntityProp.getAssociatedEntity() >> nestedAssociatedEntity
+
+ def nestedPartA = Stub(PersistentProperty)
+ def nestedPartB = Stub(PersistentProperty)
+ def perArray = [nestedPartA, nestedPartB] as PersistentProperty[]
+ nestedAssociatedEntity.getCompositeIdentity() >> perArray
+
+ // 4. Mock the behavior of the dependency methods
+ tableNameFetcher.getTableName(refDomainClass) >> "ref_table"
+ namingStrategy.resolveColumnName("nestedEntity") >> "nested_entity_col"
+ columnNameFetcher.getDefaultColumnName(nestedPartA) >> "part_a_col"
+ columnNameFetcher.getDefaultColumnName(nestedPartB) >> "part_b_col"
+
+ // Make backticks remover pass through the values for simplicity
+ backticksRemover.apply(_) >> { String s -> s }
+
+ when:
+ binder.bindCompositeIdentifierToManyToOne(association, value, compositeId, refDomainClass, path)
+
+ then:
+ // 5. Verify the final generated column names
+ def finalColumns = propertyConfig.getColumns()
+ finalColumns.size() == 2
+ finalColumns[0].getName() == "ref_table_nested_entity_col_part_a_col"
+ finalColumns[1].getName() == "ref_table_nested_entity_col_part_b_col"
+
+ and: // 6. Verify the call to the simple value binder
+ 1 * simpleValueBinder.bindSimpleValue(association, null, value, path)
+ }
+
+ def "Test bindCompositeIdentifierToManyToOne when column count matches"() {
+ given:
+ // 1. Use Mocks for dependencies that require interaction verification
+ def calculator = Stub(ForeignKeyColumnCountCalculator)
+ def tableNameFetcher = Mock(TableNameFetcher)
+ def namingStrategy = Mock(PersistentEntityNamingStrategy)
+ def columnNameFetcher = Mock(DefaultColumnNameFetcher)
+ def propertyConfigConverter = Stub(PersistentPropertyToPropertyConfig)
+ def backticksRemover = Mock(BackticksRemover)
+ def simpleValueBinder = Mock(SimpleValueBinder)
+
+ def binder = new CompositeIdentifierToManyToOneBinder(calculator, tableNameFetcher, namingStrategy, columnNameFetcher, propertyConfigConverter, backticksRemover, simpleValueBinder)
+
+ // 2. Set up arguments
+ def association = Stub(ToOne)
+ def value = Stub(SimpleValue)
+ def compositeId = new CompositeIdentity()
+ compositeId.setPropertyNames(["prop1", "prop2"] as String[])
+ def refDomainClass = Stub(PersistentEntity)
+ def path = "/test"
+
+ // 3. Set up the "match" condition
+ def propertyConfig = new PropertyConfig()
+ propertyConfig.getColumns().add(new ColumnConfig())
+ propertyConfig.getColumns().add(new ColumnConfig())
+ propertyConfigConverter.apply(association) >> propertyConfig
+
+ // The calculated length is the same as the number of columns already in the config
+ calculator.calculateForeignKeyColumnCount(refDomainClass, _ as String[]) >> 2
+
+ when:
+ binder.bindCompositeIdentifierToManyToOne(association, value, compositeId, refDomainClass, path)
+
+ then:
+ // 4. Verify the column name generation logic is skipped
+ 0 * tableNameFetcher._
+ 0 * namingStrategy._
+ 0 * columnNameFetcher._
+ 0 * backticksRemover._
+
+ and: // 5. Verify the simple value binder is still called
+ 1 * simpleValueBinder.bindSimpleValue(association, null, value, path)
+ }
+}