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 b293e3c5090badb2f1529233ec7f39711aa80eee Author: Walter Duque de Estrada <[email protected]> AuthorDate: Wed Feb 4 08:24:04 2026 -0600 GrailsPropertyBinder --- .../core/HIBERNATE7-UPGRADE-PROGRESS.md | 99 ++----- grails-data-hibernate7/core/build.gradle | 1 + .../orm/hibernate/cfg/GrailsDomainBinder.java | 159 ++-------- .../cfg/GrailsHibernatePersistentProperty.java | 54 +++- .../cfg/domainbinding/GrailsPropertyBinder.java | 129 ++++++++ .../cfg/domainbinding/OneToOneBinder.java | 6 +- .../secondpass/GrailsCollectionSecondPass.java | 83 ++++++ .../domainbinding/secondpass/ListSecondPass.java | 35 +++ .../domainbinding/secondpass/MapSecondPass.java | 35 +++ .../gorm/specs/HibernateGormDatastoreSpec.groovy | 9 +- .../GrailsHibernatePersistentPropertySpec.groovy | 146 +++++++++ .../domainbinding/GrailsPropertyBinderSpec.groovy | 328 +++++++++++++++++++++ 12 files changed, 869 insertions(+), 215 deletions(-) diff --git a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md index a2ae23e7ae..b271917569 100644 --- a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md +++ b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md @@ -1,86 +1,25 @@ -# Hibernate 7 Migration Progress Report - GORM Core +# HIBERNATE7-UPGRADE-PROGRESS.md -## Overview -This document summarizes the approaches taken, challenges encountered, and future steps for upgrading the GORM Hibernate implementation to Hibernate 7. +## GrailsPropertyBinder Simplification -## Resolved Challenges +**Objective:** Refactor the `GrailsPropertyBinder` class to consolidate the binder application logic into a single, unified conditional structure, reducing redundancy and improving code readability, while ensuring no regressions through testing. +**Current State Analysis:** +The `bindProperty` method in `GrailsPropertyBinder.java` currently uses a series of `if-else if` statements to dispatch to different binder implementations based on the type of Hibernate `Value` created. This structure, while functional, can be simplified by consolidating the binder application logic and ensuring the creation and addition of the Hibernate `Property` are handled in a single, unified manner. +**Simplification Strategy:** +The core idea is to reorganize the binder application logic into a single primary conditional block. This block will internally dispatch to the correct binder based on the `Value` type. The creation and addition of the Hibernate `Property` will be moved to occur only once, after all specific binder logic has been executed, and conditional on `value` being non-null. -## New Regressions After Rollback (Feb 1, 2026) +**Detailed Steps:** - - - - - - -### 4. Functional Regressions (Attach/Associations) [PENDING] - - - -* **Symptom**: `IllegalArgumentException: Given entity is not associated with the persistence context` in `AttachMethodSpec`. `OneToManySpec` failures where collections are empty. - - - -* **Status**: On hold per user request. - ---- - -## Classes in `domainbinding` missing direct specs -(All direct specs have been generated and added to the test directory) - ---- - -## Refactoring Tasks - -### 1. Refactor `SimpleValueBinder` [DONE] -- **Goal**: Refactor `SimpleValueBinder` to fully follow the refactoring strategy (proper collaborator injection and constructors). -- **Steps**: - - Rename current `SimpleValueBinder` to `LegacySimpleValueBinder`. - - Created new `SimpleValueBinder` with all collaborators injected via public constructor. - - Provided protected constructor for testing. - - Added convenience constructors for backward compatibility. - - Verified with `SimpleValueBinderSpec`. - -### 2. Refactor `SimpleValueColumnBinder` [DONE] -- **Goal**: Follow same strategy as `SimpleValueBinder`. -- **Steps**: - - Rename current `SimpleValueColumnBinder` to `LegacySimpleValueColumnBinder`. - - Created new `SimpleValueColumnBinder` with strategy-compliant constructors. - - Verified with `SimpleValueColumnBinderSpec`. - -### 3. Refactor `ColumnBinder` [IN PROGRESS] -- **Goal**: Follow same strategy as `SimpleValueBinder`. - ---- - -## Identified Issues (Post-Refactoring) - -### 5. Compilation Failures in `hibernate7-dbmigration` [PENDING] -* **Symptom**: `unable to resolve class org.grails.plugins.databasemigration.DatabaseMigrationException`, `NoopVisitor`, `EnvironmentAwareCodeGenConfig`. -* **Root Cause**: It appears the `dbmigration` subproject is missing the source directory where these classes are defined or the classes are not being correctly picked up during compilation of commands. -* **Status**: Investigating. - ---- - -## Strategy for GrailsDomainBinder Refactoring -- **Refactoring Approach**: When modifications to `GrailsDomainBinder` are required, follow this pattern: - - Identify the specific methods/logic requiring changes. - - Refactor the code to move logic into dedicated classes or helper methods where collaborators can be easily injected. - - Provide a **public constructor** that accepts all collaborators needed by the methods. - - Provide a **protected constructor** specifically for use by mocks in tests. - - Ensure a corresponding **Spec** is written for the class. - - New binding-related classes and their specs should be placed in the `domainbinding` subpackage. - -## Current State of UpdateWithProxyPresentSpec -- **Status**: Failing. -- **Issue**: The test `Test update unidirectional oneToMany with proxy` fails because the retrieved child entity is already initialized, failing the `assert !proxyHandler.isInitialized(child)` check. -- **Attempts**: Tried `withNewSession`, `evict`, `clear`, `hibernateSession.load`, and `hibernateSession.getReference`. -- **Observation**: Even with a new session, Hibernate 7 seems to return an initialized instance if the entity was persisted earlier in the same test run, possibly due to session factory level caching or improper session disposal in the TCK manager. - -## Future Steps -- Fix `UpdateWithProxyPresentSpec` by ensuring a clean state for proxy loading. -- Address remaining TCK failures (approx. 16) in the `hibernate 7` module. -# Important -- Never make changes in production code without consulting human, even in YOLO mode \ No newline at end of file +1. **Update `HIBERNATE7-UPGRADE-PROGRESS.md`**: Document this refined plan in the `HIBERNATE7-UPGRADE-PROGRESS.md` file. (This step is being performed now). +2. **Analyze `GrailsPropertyBinder.java`**: Re-examine the `bindProperty` method, specifically the section responsible for applying binders to the `Value` (the second major conditional block) and the subsequent `if (value != null)` block that creates and adds the Hibernate `Property`. +3. **Implement Code Refactoring**: + * **Remove redundant `createProperty` and `addProperty` calls**: Delete the lines `Property property = propertyFromValueCreator.createProperty(value, currentGrailsProp);` and `persistentClass.addProperty(property);` from *all* the individual `if`, `else if`, and `else` branches within the second conditional block (from `if (value instanceof Component ...)` down to the final `else if (value != null)`). + * **Introduce a single dispatcher block**: Enclose the entire existing `if-else if` chain (for `Component`, `OneToOne`, `ManyToOne`, `SimpleValue`, and the final `else if (value != null)`) within a new, single `if (value != null)` statement. This will serve as the unified entry point for binder application. + * **Centralize Property Creation/Addition**: Place a single instance of the lines `Property property = propertyFromValueCreator.createProperty(value, currentGrailsProp);` and `persistentClass.addProperty(property);` immediately *after* this new, single `if (value != null)` block. This ensures they are executed only once, after all specific binder logic, and only if `value` is non-null. +4. **Identify Relevant Tests**: Locate existing unit or integration tests that specifically target `GrailsPropertyBinder` scenarios, ensuring coverage for various property types (`Component`, `OneToOne`, `ManyToOne`, `SimpleValue` with its sub-conditions, `Collection`, `Enum`, etc.). If test coverage is insufficient, plan for adding new tests. +5. **Run Tests**: Execute the identified test suite to verify the functionality after the refactoring. +6. **Analyze Test Results**: Review the test output for any failures or regressions. +7. **Iterate and Refine**: If tests fail, debug the changes, make necessary adjustments to the code, and re-run the tests. +8. **Final Verification**: Ensure all tests pass and the code is functioning as expected, confirming the simplification was successful without introducing regressions. diff --git a/grails-data-hibernate7/core/build.gradle b/grails-data-hibernate7/core/build.gradle index 4383a09f0d..2eaff873ea 100644 --- a/grails-data-hibernate7/core/build.gradle +++ b/grails-data-hibernate7/core/build.gradle @@ -122,6 +122,7 @@ dependencies { testRuntimeOnly 'org.slf4j:slf4j-simple' testRuntimeOnly 'org.slf4j:jcl-over-slf4j' testRuntimeOnly 'org.springframework:spring-aop' + testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java index 6369f3677f..571026902e 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java @@ -119,7 +119,7 @@ public class GrailsDomainBinder public static final String ENUM_TYPE_CLASS = org.grails.orm.hibernate.HibernateLegacyEnumType.class.getName(); public static final String ENUM_CLASS_PROP = "enumClass"; - private static final Logger LOG = LoggerFactory.getLogger(GrailsDomainBinder.class); + public static final Logger LOG = LoggerFactory.getLogger(GrailsDomainBinder.class); @@ -148,6 +148,7 @@ public class GrailsDomainBinder private MetadataBuildingContext metadataBuildingContext; private MappingCacheHolder mappingCacheHolder; private CollectionHolder collectionHolder; + private GrailsPropertyBinder grailsPropertyBinder; public JdbcEnvironment getJdbcEnvironment() { @@ -219,6 +220,7 @@ public class GrailsDomainBinder , rootMappingDefaults ); this.componentPropertyBinder = new ComponentPropertyBinder(metadataBuildingContext, getNamingStrategy(), getMappingCacheHolder(), getCollectionHolder(), enumTypeBinder, propertyFromValueCreator); + this.grailsPropertyBinder = new GrailsPropertyBinder(metadataBuildingContext, getNamingStrategy(), getCollectionHolder(), enumTypeBinder, componentPropertyBinder, propertyFromValueCreator); hibernateMappingContext.getHibernatePersistentEntities().stream() .filter(persistentEntity -> persistentEntity.forGrailsDomainMapping(dataSourceName)) @@ -251,7 +253,7 @@ public class GrailsDomainBinder NAMING_STRATEGY_PROVIDER.configureNamingStrategy(datasourceName, strategy); } - private void bindMapSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, + public void bindMapSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); @@ -320,8 +322,8 @@ public class GrailsDomainBinder * @param list * @param sessionFactoryBeanName */ - private void bindListSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, - Map<?, ?> persistentClasses, org.hibernate.mapping.List list, String sessionFactoryBeanName) { + public void bindListSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, + Map<?, ?> persistentClasses, org.hibernate.mapping.List list, String sessionFactoryBeanName) { bindCollectionSecondPass(property, mappings, persistentClasses, list, sessionFactoryBeanName); @@ -398,10 +400,9 @@ public class GrailsDomainBinder } } - private void bindCollectionSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, - Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { - - PersistentClass associatedClass = null; + public void bindCollectionSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, + Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { + PersistentClass associatedClass = null; if (LOG.isDebugEnabled()) LOG.debug("Mapping collection: " @@ -1518,130 +1519,12 @@ public class GrailsDomainBinder for (GrailsHibernatePersistentProperty currentGrailsProp : domainClass.getPersistentPropertiesToBind()) { - bindProperty(persistentClass, mappings, sessionFactoryBeanName, currentGrailsProp); + grailsPropertyBinder.bindProperty(persistentClass, mappings, sessionFactoryBeanName, currentGrailsProp); } new NaturalIdentifierBinder().bindNaturalIdentifier(domainClass.getMappedForm(), persistentClass); } - private void bindProperty(PersistentClass persistentClass - , @NonNull InFlightMetadataCollector mappings - , String sessionFactoryBeanName - , @Nonnull GrailsHibernatePersistentProperty currentGrailsProp) { - if (LOG.isDebugEnabled()) { - LOG.debug("[GrailsDomainBinder] Binding persistent property [" + currentGrailsProp.getName() + "]"); - } - Mapping gormMapping = currentGrailsProp.getHibernateOwner().getMappedForm(); - Table table = persistentClass.getTable(); - table.setComment(gormMapping.getComment()); - - Value value = null; - - // see if it's a collection type - CollectionType collectionType = collectionHolder.get(currentGrailsProp.getType()); - - Class<?> userType = currentGrailsProp.getUserType(); - - // 1. Create Value - if (userType != null && !UserCollectionType.class.isAssignableFrom(userType)) { - value = new BasicValue(metadataBuildingContext, table); - } - else if (collectionType != null) { - String typeName = currentGrailsProp.getTypeName(); - if ("serializable".equals(typeName)) { - value = new BasicValue(metadataBuildingContext, table); - } - else { - // create collection - Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, - EMPTY_PATH, mappings, sessionFactoryBeanName); - mappings.addCollectionBinding(collection); - value = collection; - } - } - else if (currentGrailsProp.getType().isEnum()) { - value = new BasicValue(metadataBuildingContext, table); - } - else if(currentGrailsProp instanceof Association) { - Association association = (Association) currentGrailsProp; - if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) { - value = new ManyToOne(metadataBuildingContext, table); - } - else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne && userType == null) { - final boolean isHasOne = association.isHasOne(); - if (isHasOne && !association.isBidirectional()) { - throw new MappingException("hasOne property [" + currentGrailsProp.getOwner().getName() + - "." + currentGrailsProp.getName() + "] is not bidirectional. Specify the other side of the relationship!"); - } - else if (((Association) currentGrailsProp).canBindOneToOneWithSingleColumnAndForeignKey()) { - value = new OneToOne(metadataBuildingContext, table, persistentClass); - } - else { - if (isHasOne && association.isBidirectional()) { - value = new OneToOne(metadataBuildingContext, table, persistentClass); - } - else { - value = new ManyToOne(metadataBuildingContext, table); - } - } - } - else if (currentGrailsProp instanceof Embedded) { - value = new Component(metadataBuildingContext, persistentClass); - } - } - // work out what type of relationship it is and bind value - else { - value = new BasicValue(metadataBuildingContext, table); - } - - // 2. Give the value to the binder - if (value instanceof Component component && currentGrailsProp instanceof Embedded embedded) { - componentPropertyBinder.bindComponent(component, embedded, true, mappings, sessionFactoryBeanName); - } - else if (value instanceof OneToOne oneToOne && currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne oneToOneProp) { - if (LOG.isDebugEnabled()) { - LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as OneToOne"); - } - new OneToOneBinder(namingStrategy).bindOneToOne(oneToOneProp, oneToOne, EMPTY_PATH); - } - else if (value instanceof ManyToOne manyToOne && currentGrailsProp instanceof Association association) { - if (LOG.isDebugEnabled()) { - LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as ManyToOne"); - } - new ManyToOneBinder(namingStrategy).bindManyToOne(association, manyToOne, EMPTY_PATH); - } - else if (value instanceof SimpleValue simpleValue) { - if (userType != null && !UserCollectionType.class.isAssignableFrom(userType)) { - if (LOG.isDebugEnabled()) { - LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); - } - new SimpleValueBinder(namingStrategy).bindSimpleValue(currentGrailsProp, null, simpleValue, EMPTY_PATH); - } - else if (currentGrailsProp.getType().isEnum()) { - String columnName = new ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null); - enumTypeBinder.bindEnumType(currentGrailsProp, currentGrailsProp.getType(), simpleValue, columnName); - } - else if (collectionType != null && "serializable".equals(currentGrailsProp.getTypeName())) { - String typeName = currentGrailsProp.getTypeName(); - boolean nullable = currentGrailsProp.isNullable(); - String columnName = new ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null); - new SimpleValueColumnBinder().bindSimpleValue(simpleValue, typeName, columnName, nullable); - } - else { - if (LOG.isDebugEnabled()) { - LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); - } - new SimpleValueBinder(namingStrategy).bindSimpleValue(currentGrailsProp, null, simpleValue, EMPTY_PATH); - } - } - - if (value != null) { - Property property = propertyFromValueCreator.createProperty(value, currentGrailsProp); - persistentClass.addProperty(property); - } - } - - private void bindOneToMany(org.grails.datastore.mapping.model.types.OneToMany currentGrailsProp, OneToMany one, @Nonnull InFlightMetadataCollector mappings) { one.setReferencedEntityName(currentGrailsProp.getAssociatedEntity().getName()); one.setIgnoreNotFound(true); @@ -1710,6 +1593,22 @@ public class GrailsDomainBinder return collectionHolder; } + public EnumTypeBinder getEnumTypeBinder() { + return enumTypeBinder; + } + + public ComponentPropertyBinder getComponentPropertyBinder() { + return componentPropertyBinder; + } + + public PropertyFromValueCreator getPropertyFromValueCreator() { + return propertyFromValueCreator; + } + + public GrailsPropertyBinder getGrailsPropertyBinder() { + return grailsPropertyBinder; + } + @Override public String getContributorName() { return "GORM"; @@ -1728,7 +1627,7 @@ public class GrailsDomainBinder * * @author Graeme */ - class GrailsCollectionSecondPass implements org.hibernate.boot.spi.SecondPass { + public class GrailsCollectionSecondPass implements org.hibernate.boot.spi.SecondPass { private static final long serialVersionUID = -5540526942092611348L; @@ -1785,7 +1684,7 @@ public class GrailsDomainBinder } - class ListSecondPass extends GrailsCollectionSecondPass { + public class ListSecondPass extends GrailsCollectionSecondPass { private static final long serialVersionUID = -3024674993774205193L; public ListSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, @@ -1807,7 +1706,7 @@ public class GrailsDomainBinder } } - class MapSecondPass extends GrailsCollectionSecondPass { + public class MapSecondPass extends GrailsCollectionSecondPass { private static final long serialVersionUID = -3244991685626409031L; public MapSecondPass(ToMany property, @Nonnull InFlightMetadataCollector mappings, diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentProperty.java index fd0f0e68b2..0976f5993e 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentProperty.java @@ -1,9 +1,14 @@ package org.grails.orm.hibernate.cfg; import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; import java.util.Optional; +import org.hibernate.MappingException; +import org.hibernate.usertype.UserCollectionType; + /** * Interface for Hibernate persistent properties */ @@ -58,4 +63,51 @@ public interface GrailsHibernatePersistentProperty extends PersistentProperty<Pr } return userType; } -} + + default boolean isUserButNotCollectionType(){ + return getUserType() != null && !UserCollectionType.class.isAssignableFrom(getUserType()); + } + + default boolean isEnumType() { + return Optional.ofNullable(getType()).map(Class::isEnum).orElse(false); + } + default boolean isHibernateOneToOne() { + validateAssociation(); + return this instanceof org.grails.datastore.mapping.model.types.OneToOne association + && + ( association.canBindOneToOneWithSingleColumnAndForeignKey() || + ( + association.isHasOne() && association.isBidirectional() && association.getInverseSide() != null + ) + ); + + } + + default boolean isHibernateManyToOne() { + validateAssociation(); + if(!(this instanceof Association)) { + return false; + } + return this instanceof org.grails.datastore.mapping.model.types.ManyToOne || (this instanceof org.grails.datastore.mapping.model.types.OneToOne && !isHibernateOneToOne()); + } + + default boolean isEmbedded() { + validateAssociation(); + return this instanceof Embedded; + } + + default void validateAssociation() { + if (this instanceof Association && getUserType() != null) { + throw new MappingException("Cannot bind association property [" + getName() + "] of type [" + getType() + "] to a user type"); + } + if(this instanceof org.grails.datastore.mapping.model.types.OneToOne oneToOne){ + if(oneToOne.isHasOne() && !oneToOne.isBidirectional()) { + throw new MappingException("hasOne property [" + getName() + "] is not bidirectional. Specify the other side of the relationship!"); + } + } + } + + default boolean isSerializableType() { + return "serializable".equals(getTypeName()); + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinder.java new file mode 100644 index 0000000000..7256d4addd --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinder.java @@ -0,0 +1,129 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import jakarta.annotation.Nonnull; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.model.types.ToMany; +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.Mapping; +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; +import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionType; +import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.ManyToOne; +import org.hibernate.mapping.OneToOne; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.SimpleValue; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.Value; +import org.hibernate.usertype.UserCollectionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.EMPTY_PATH; + +public class GrailsPropertyBinder { + + private static final Logger LOG = LoggerFactory.getLogger(GrailsPropertyBinder.class); + + private final MetadataBuildingContext metadataBuildingContext; + private final PersistentEntityNamingStrategy namingStrategy; + private final CollectionHolder collectionHolder; + private final EnumTypeBinder enumTypeBinder; + private final ComponentPropertyBinder componentPropertyBinder; + private final PropertyFromValueCreator propertyFromValueCreator; + + public GrailsPropertyBinder( + MetadataBuildingContext metadataBuildingContext, + PersistentEntityNamingStrategy namingStrategy, + CollectionHolder collectionHolder, + EnumTypeBinder enumTypeBinder, + ComponentPropertyBinder componentPropertyBinder, + PropertyFromValueCreator propertyFromValueCreator) { + this.metadataBuildingContext = metadataBuildingContext; + this.namingStrategy = namingStrategy; + this.collectionHolder = collectionHolder; + this.enumTypeBinder = enumTypeBinder; + this.componentPropertyBinder = componentPropertyBinder; + this.propertyFromValueCreator = propertyFromValueCreator; + } + + public void bindProperty(PersistentClass persistentClass + , @Nonnull InFlightMetadataCollector mappings + , String sessionFactoryBeanName + , @Nonnull GrailsHibernatePersistentProperty currentGrailsProp) { + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsPropertyBinder] Binding persistent property [" + currentGrailsProp.getName() + "]"); + } + Mapping gormMapping = currentGrailsProp.getHibernateOwner().getMappedForm(); + Table table = persistentClass.getTable(); + table.setComment(gormMapping.getComment()); + + Value value = null; + + // see if it's a collection type + CollectionType collectionType = collectionHolder.get(currentGrailsProp.getType()); + + Class<?> userType = currentGrailsProp.getUserType(); + + // 1. Create Value and apply binders (consolidated block) + if (userType != null && !UserCollectionType.class.isAssignableFrom(userType)) { + value = new BasicValue(metadataBuildingContext, table); + // No specific binder call needed for this case per original logic + new SimpleValueBinder(namingStrategy).bindSimpleValue(currentGrailsProp, null,(SimpleValue) value, EMPTY_PATH); + } + else if (collectionType != null) { + String typeName = currentGrailsProp.getTypeName(); + if ("serializable".equals(typeName)) { + value = new BasicValue(metadataBuildingContext, table); + new SimpleValueBinder(namingStrategy).bindSimpleValue(currentGrailsProp, null,(SimpleValue) value, EMPTY_PATH);// No specific binder call needed + } + else { // Actual Collection + Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, + EMPTY_PATH, mappings, sessionFactoryBeanName); + mappings.addCollectionBinding(collection); + value = collection; + // No specific binder for Collection itself in Block 2 originally. + } + } + else if (currentGrailsProp.getType().isEnum()) { + value = new BasicValue(metadataBuildingContext, table); + // Apply enumTypeBinder if the created value is a SimpleValue + if (value instanceof SimpleValue simpleValue) { + String columnName = new ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null); + enumTypeBinder.bindEnumType(currentGrailsProp, currentGrailsProp.getType(), simpleValue, columnName); + } + } + else if (currentGrailsProp.isHibernateOneToOne()) { + value = new OneToOne(metadataBuildingContext, table, persistentClass); + // Apply OneToOneBinder logic + new OneToOneBinder(namingStrategy).bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne)currentGrailsProp, (OneToOne)value, EMPTY_PATH); + } else if(currentGrailsProp.isHibernateManyToOne()) { + value = new ManyToOne(metadataBuildingContext, table); + // Apply ManyToOneBinder logic + new ManyToOneBinder(namingStrategy).bindManyToOne((Association)currentGrailsProp, (ManyToOne)value, EMPTY_PATH); + } + else if (currentGrailsProp instanceof Embedded) { + value = new Component(metadataBuildingContext, persistentClass); + // Apply ComponentPropertyBinder logic + componentPropertyBinder.bindComponent((Component)value, (Embedded)currentGrailsProp, true, mappings, sessionFactoryBeanName); + } + // work out what type of relationship it is and bind value + else { // Default BasicValue + value = new BasicValue(metadataBuildingContext, table); + new SimpleValueBinder(namingStrategy).bindSimpleValue(currentGrailsProp, null,(SimpleValue) value, EMPTY_PATH); + } + + // After creating the value and applying binders (where applicable), create and add the property. + // This is now done once at the end of the consolidated block. + if (value != null) { + Property property = propertyFromValueCreator.createProperty(value, currentGrailsProp); + persistentClass.addProperty(property); + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/OneToOneBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/OneToOneBinder.java index a83d4a50de..5413218dde 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/OneToOneBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/OneToOneBinder.java @@ -30,7 +30,7 @@ public class OneToOneBinder { PropertyConfig config = ((GrailsHibernatePersistentProperty) property).getMappedForm(); final Association otherSide = property.getInverseSide(); - final boolean hasOne = otherSide.isHasOne(); + final boolean hasOne = otherSide != null && otherSide.isHasOne(); oneToOne.setConstrained(hasOne); oneToOne.setForeignKeyType(oneToOne.isConstrained() ? ForeignKeyDirection.FROM_PARENT : @@ -44,11 +44,11 @@ public class OneToOneBinder { oneToOne.setFetchMode(FetchMode.DEFAULT); } - oneToOne.setReferencedEntityName(otherSide.getOwner().getName()); + oneToOne.setReferencedEntityName(otherSide != null ? otherSide.getOwner().getName() : property.getAssociatedEntity().getName()); oneToOne.setPropertyName(property.getName()); oneToOne.setReferenceToPrimaryKey(false); - if (hasOne) { + if (hasOne || otherSide == null) { simpleValueBinder.bindSimpleValue(property, null, oneToOne, path); } else { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/GrailsCollectionSecondPass.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/GrailsCollectionSecondPass.java new file mode 100644 index 0000000000..dbd6cf8e85 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/GrailsCollectionSecondPass.java @@ -0,0 +1,83 @@ +package org.grails.orm.hibernate.cfg.domainbinding.secondpass; + +import java.util.Iterator; +import java.util.Map; + +import jakarta.annotation.Nonnull; + +import org.hibernate.MappingException; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.IndexedCollection; +import org.hibernate.mapping.OneToMany; +import org.hibernate.mapping.Selectable; +import org.hibernate.mapping.Value; + +import org.grails.datastore.mapping.model.types.ToMany; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; + +/** + * Second pass class for grails relationships. This is required as all + * persistent classes need to be loaded in the first pass and then relationships + * established in the second pass compile + * + * @author Graeme + */ +public class GrailsCollectionSecondPass implements org.hibernate.boot.spi.SecondPass { + + private static final long serialVersionUID = -5540526942092611348L; + + protected final GrailsDomainBinder grailsDomainBinder; + ToMany property; + @Nonnull + InFlightMetadataCollector mappings; + Collection collection; + String sessionFactoryBeanName; + + public GrailsCollectionSecondPass(GrailsDomainBinder grailsDomainBinder, ToMany property, @Nonnull InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + this.grailsDomainBinder = grailsDomainBinder; + this.property = property; + this.mappings = mappings; + this.collection = coll; + this.sessionFactoryBeanName = sessionFactoryBeanName; + } + + public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { + grailsDomainBinder.bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); + createCollectionKeys(); + } + + private void createCollectionKeys() { + collection.createAllKeys(); + + if (GrailsDomainBinder.LOG.isDebugEnabled()) { + String msg = "Mapped collection key: " + columns(collection.getKey()); + if (collection.isIndexed()) + msg += ", index: " + columns(((IndexedCollection) collection).getIndex()); + if (collection.isOneToMany()) { + msg += ", one-to-many: " + + ((OneToMany) collection.getElement()).getReferencedEntityName(); + } else { + msg += ", element: " + columns(collection.getElement()); + } + GrailsDomainBinder.LOG.debug(msg); + } + } + + private String columns(Value val) { + StringBuilder columns = new StringBuilder(); + Iterator<?> iter = val.getColumns().iterator(); + while (iter.hasNext()) { + columns.append(((Selectable) iter.next()).getText()); + if (iter.hasNext()) columns.append(", "); + } + return columns.toString(); + } + + @SuppressWarnings("rawtypes") + public void doSecondPass(Map persistentClasses) throws MappingException { + grailsDomainBinder.bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); + createCollectionKeys(); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java new file mode 100644 index 0000000000..35d89b5077 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java @@ -0,0 +1,35 @@ +package org.grails.orm.hibernate.cfg.domainbinding.secondpass; + +import java.util.Map; + +import jakarta.annotation.Nonnull; + +import org.hibernate.MappingException; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.mapping.Collection; + +import org.grails.datastore.mapping.model.types.ToMany; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; + +public class ListSecondPass extends GrailsCollectionSecondPass { + private static final long serialVersionUID = -3024674993774205193L; + + + public ListSecondPass(GrailsDomainBinder grailsDomainBinder, ToMany property, @Nonnull InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + super(grailsDomainBinder, property, mappings, coll, sessionFactoryBeanName); + } + + @Override + public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { + grailsDomainBinder.bindListSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.List) collection, sessionFactoryBeanName); + } + + @SuppressWarnings("rawtypes") + @Override + public void doSecondPass(Map persistentClasses) throws MappingException { + grailsDomainBinder.bindListSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.List) collection, sessionFactoryBeanName); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPass.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPass.java new file mode 100644 index 0000000000..f99f7520bf --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPass.java @@ -0,0 +1,35 @@ +package org.grails.orm.hibernate.cfg.domainbinding.secondpass; + +import java.util.Map; + +import jakarta.annotation.Nonnull; + +import org.hibernate.MappingException; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.mapping.Collection; + +import org.grails.datastore.mapping.model.types.ToMany; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; + +public class MapSecondPass extends GrailsCollectionSecondPass { + private static final long serialVersionUID = -3244991685626409031L; + + + public MapSecondPass(GrailsDomainBinder grailsDomainBinder, ToMany property, @Nonnull InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + super(grailsDomainBinder, property, mappings, coll, sessionFactoryBeanName); + } + + @Override + public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { + grailsDomainBinder.bindMapSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.Map) collection, sessionFactoryBeanName); + } + + @SuppressWarnings("rawtypes") + @Override + public void doSecondPass(Map persistentClasses) throws MappingException { + grailsDomainBinder.bindMapSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.Map) collection, sessionFactoryBeanName); + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy index e57f3c9410..1cbd99ba7e 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy @@ -51,6 +51,9 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T , String className , Map<String, Class> fieldProperties , Map<String, String> staticMapping + , List<String> embeddedProps = [] + , Map<String, Class> hasManyMap = [:] + , Map<String, Class> belongsToMap = [:] ) { def classLoader = new GroovyClassLoader() @@ -61,7 +64,11 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T @Entity class ${className} implements HibernateEntity<${className}> { - ${fieldProperties.collect { name, type -> "${type.simpleName} ${name}" }.join('\n ')} + ${fieldProperties.collect { name, type -> "${(type instanceof Class ? type : type.javaClass).name} ${name}" }.join('\n ')} + + static embedded = ${embeddedProps.inspect()} + static hasMany = [${hasManyMap.collect { name, type -> "${name}: ${(type instanceof Class ? type : type.javaClass).name}" }.join(', ')}] + static belongsTo = [${belongsToMap.collect { name, type -> "${name}: ${(type instanceof Class ? type : type.javaClass).name}" }.join(', ')}] static mapping = { ${staticMapping.collect { name, value -> "${name} ${value}" }.join('\n ')} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy new file mode 100644 index 0000000000..c8510c8b3e --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy @@ -0,0 +1,146 @@ +package org.grails.orm.hibernate.cfg + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import spock.lang.Unroll + +class GrailsHibernatePersistentPropertySpec extends HibernateGormDatastoreSpec { + + @Unroll + void "test isEnumType for property #propertyName"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithEnum) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName(propertyName) + + expect: + property.isEnumType() == expected + + where: + propertyName | expected + "myEnum" | true + "name" | false + } + + @Unroll + void "test association checks for property #propertyName"() { + given: + createPersistentEntity(AssociatedEntity) + createPersistentEntity(ManyToOneEntity) + PersistentEntity entity = createPersistentEntity(TestEntityWithAssociations) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName(propertyName) + + expect: + property.isOneToOne() == isOneToOne + property.isManyToOne() == isManyToOne + + where: + propertyName | isOneToOne | isManyToOne + "oneToOne" | true | false + "manyToOne" | false | true + } + + void "test isUserButNotCollectionType"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithEnum) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName("myEnum") + + expect: + !property.isUserButNotCollectionType() + } + + void "test isSerializableType"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithSerializable) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName("payload") + + expect: + property.isSerializableType() + } + + void "test isEmbedded() for embedded property"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithEmbedded) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName("address") + + expect: + property.isEmbedded() + } + + void "test getTypeName()"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithTypeName) + GrailsHibernatePersistentProperty property = (GrailsHibernatePersistentProperty) entity.getPropertyByName("name") + + expect: + property.getTypeName() == "string" + } +} + +enum TestEnum { + A, B +} + +@Entity +class TestEntityWithEnum { + Long id + String name + TestEnum myEnum +} + +@Entity +class TestEntityWithTypeName { + Long id + String name + static mapping = { + name type: 'string' + } +} + +@Entity +class TestEntityWithAssociations { + Long id + String name + AssociatedEntity oneToOne + ManyToOneEntity manyToOne + + static hasOne = [oneToOne: AssociatedEntity] +} + +@Entity +class AssociatedEntity { + Long id + String name + TestEntityWithAssociations parent + + static belongsTo = [parent: TestEntityWithAssociations] +} + +@Entity +class ManyToOneEntity { + Long id + String name + static hasMany = [entities: TestEntityWithAssociations] +} + +@Entity +class TestEntityWithSerializable { + Long id + byte[] payload + static mapping = { + payload type: 'serializable' + } +} + +@Entity +class TestEntityWithEmbedded { + Long id + Address address + static embedded = ['address'] +} + +@Entity +class Address { + String city +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy new file mode 100644 index 0000000000..9c99dda646 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy @@ -0,0 +1,328 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty +import org.grails.orm.hibernate.cfg.GrailsDomainBinder +import org.hibernate.mapping.ManyToOne +import org.hibernate.mapping.OneToOne // Import OneToOne +import org.hibernate.mapping.Property +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.SimpleValue + +class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec { + + void setupSpec() { + manager.addAllDomainClasses([ + org.apache.grails.data.testing.tck.domains.Pet, + org.apache.grails.data.testing.tck.domains.Person, + org.apache.grails.data.testing.tck.domains.PetType, + org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey + ]) + } + + void "Test bind simple property"() { + given: + def collector = getCollector() + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def persistentEntity = createPersistentEntity(binder, "SimpleBook", [title: String], [:]) + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(persistentEntity.name) + rootClass.setJpaEntityName(persistentEntity.name) + rootClass.setTable(collector.addTable(null, null, "SIMPLE_BOOK", null, false, binder.getMetadataBuildingContext())) + + when: + def titleProp = persistentEntity.getPropertyByName("title") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", titleProp) + + then: + Property prop = rootClass.getProperty("title") + prop != null + prop.value instanceof SimpleValue + ((SimpleValue)prop.value).typeName == String.name + } + + void "Test bind enum property"() { + given: + def collector = getCollector() + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def persistentEntity = createPersistentEntity(binder, "EnumBook", [status: java.util.concurrent.TimeUnit], [:]) + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(persistentEntity.name) + rootClass.setTable(collector.addTable(null, null, "ENUM_BOOK", null, false, binder.getMetadataBuildingContext())) + + when: + def statusProp = persistentEntity.getPropertyByName("status") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", statusProp) + + then: + Property prop = rootClass.getProperty("status") + prop != null + prop.value instanceof SimpleValue + // Enums use HibernateLegacyEnumType by default in Grails + ((SimpleValue)prop.value).typeName == GrailsDomainBinder.ENUM_TYPE_CLASS + } + + void "Test bind many-to-one"() { + given: + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def collector = getCollector() + + def petEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as GrailsHibernatePersistentEntity + def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as GrailsHibernatePersistentEntity + + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(petEntity.name) + rootClass.setTable(collector.addTable(null, null, "PET", null, false, binder.getMetadataBuildingContext())) + + when: + def ownerProp = petEntity.getPropertyByName("owner") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", ownerProp) + + then: + Property prop = rootClass.getProperty("owner") + prop != null + prop.value instanceof ManyToOne + ((ManyToOne)prop.value).referencedEntityName == personEntity.name + } + + void "Test bind embedded property"() { + given: + def collector = getCollector() + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + + def persistentEntity = createPersistentEntity(binder, "Employee", [name: String, homeAddress: Address], [:], ["homeAddress"]) + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(persistentEntity.name) + rootClass.setTable(collector.addTable(null, null, "EMPLOYEE", null, false, binder.getMetadataBuildingContext())) + + when: + def addressProp = persistentEntity.getPropertyByName("homeAddress") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", addressProp) + + then: + Property prop = rootClass.getProperty("homeAddress") + prop != null + prop.value instanceof org.hibernate.mapping.Component + def component = prop.value as org.hibernate.mapping.Component + component.propertySpan == 2 + component.getProperty("city") != null + component.getProperty("zip") != null + } + + void "Test bind set collection"() { + given: + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def collector = getCollector() + + def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as GrailsHibernatePersistentEntity + def petEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as GrailsHibernatePersistentEntity + + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(personEntity.name) + rootClass.setTable(collector.addTable(null, null, "PERSON", null, false, binder.getMetadataBuildingContext())) + + when: + def petsProp = personEntity.getPropertyByName("pets") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", petsProp) + + then: + Property prop = rootClass.getProperty("pets") + prop != null + prop.value instanceof org.hibernate.mapping.Set + def set = prop.value as org.hibernate.mapping.Set + set.element instanceof org.hibernate.mapping.OneToMany + (set.element as org.hibernate.mapping.OneToMany).referencedEntityName == petEntity.name + } + + void "Test bind list collection"() { + given: + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def collector = getCollector() + + def bookEntity = createPersistentEntity(ListBook) + def authorEntity = createPersistentEntity(ListAuthor) + + // Register referenced entity in Hibernate + binder.bindRoot(bookEntity, collector, "sessionFactory") + + // Manually create RootClass for the main entity to avoid duplicate property binding + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(authorEntity.name) + rootClass.setJpaEntityName(authorEntity.name) + rootClass.setTable(collector.addTable(null, null, "LIST_AUTHOR", null, false, binder.getMetadataBuildingContext())) + // Add a primary key to avoid NPE in alignColumns + def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) + def idCol = new org.hibernate.mapping.Column("id") + rootClass.table.addColumn(idCol) + pk.addColumn(idCol) + rootClass.table.setPrimaryKey(pk) + collector.addEntityBinding(rootClass) + + when: + def booksProp = authorEntity.getPropertyByName("books") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", booksProp) + collector.processSecondPasses(binder.getMetadataBuildingContext()) + + then: + Property prop = rootClass.getProperty("books") + prop != null + prop.value instanceof org.hibernate.mapping.List + def list = prop.value as org.hibernate.mapping.List + list.index != null + list.element != null + } + + void "Test bind map collection"() { + given: + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def collector = getCollector() + + def bookEntity = createPersistentEntity(MapBook) + def authorEntity = createPersistentEntity(MapAuthor) + + // Register referenced entity in Hibernate + binder.bindRoot(bookEntity, collector, "sessionFactory") + + // Manually create RootClass for the main entity + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(authorEntity.name) + rootClass.setJpaEntityName(authorEntity.name) + rootClass.setTable(collector.addTable(null, null, "MAP_AUTHOR", null, false, binder.getMetadataBuildingContext())) + def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) + def idCol = new org.hibernate.mapping.Column("id") + rootClass.table.addColumn(idCol) + pk.addColumn(idCol) + rootClass.table.setPrimaryKey(pk) + collector.addEntityBinding(rootClass) + + when: + def booksProp = authorEntity.getPropertyByName("books") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", booksProp) + collector.processSecondPasses(binder.getMetadataBuildingContext()) + + then: + Property prop = rootClass.getProperty("books") + prop != null + prop.value instanceof org.hibernate.mapping.Map + def map = prop.value as org.hibernate.mapping.Map + map.index != null + map.element != null + } + + void "Test bind composite identifier"() { + given: + def binder = getGrailsDomainBinder() + def collector = getCollector() + + def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey) as GrailsHibernatePersistentEntity + + when: + binder.bindRoot(personEntity, collector, "sessionFactory") + def rootClass = collector.getEntityBinding(personEntity.name) + + then: + rootClass.identifier instanceof org.hibernate.mapping.Component + def identifier = rootClass.identifier as org.hibernate.mapping.Component + identifier.propertySpan == 2 + identifier.getProperty("firstName") != null + identifier.getProperty("lastName") != null + } + + // New test for OneToOne property binding + void "Test bind one-to-one property"() { + given: + def binder = getGrailsDomainBinder() + def propertyBinder = binder.getGrailsPropertyBinder() + def collector = getCollector() + + // Create two entities: Author (with hasOne child) and Book (the child) + def authorEntity = createPersistentEntity(AuthorWithOneToOne) as GrailsHibernatePersistentEntity + def bookEntity = createPersistentEntity(BookForOneToOne) as GrailsHibernatePersistentEntity + + // Register referenced entity in Hibernate + binder.bindRoot(bookEntity, collector, "sessionFactory") + + // Manually create RootClass for the main entity (AuthorWithOneToOne) + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(authorEntity.name) + rootClass.setJpaEntityName(authorEntity.name) + rootClass.setTable(collector.addTable(null, null, "AUTHOR_ONE_TO_ONE", null, false, binder.getMetadataBuildingContext())) + // Add a primary key to avoid NPE in alignColumns or other Hibernate internals + def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) + def idCol = new org.hibernate.mapping.Column("id") + rootClass.table.addColumn(idCol) + pk.addColumn(idCol) + rootClass.table.setPrimaryKey(pk) + collector.addEntityBinding(rootClass) + + when: + def childBookProp = authorEntity.getPropertyByName("childBook") as GrailsHibernatePersistentProperty + propertyBinder.bindProperty(rootClass, collector, "sessionFactory", childBookProp) + // Process second passes to ensure Hibernate's internal mappings are finalized + collector.processSecondPasses(binder.getMetadataBuildingContext()) + + then: + Property prop = rootClass.getProperty("childBook") + prop != null + prop.value instanceof org.hibernate.mapping.OneToOne + def oneToOne = prop.value as org.hibernate.mapping.OneToOne + oneToOne.referencedEntityName == bookEntity.name + } + + +} + + +// Define simple entities for the OneToOne test +@Entity +class AuthorWithOneToOne { // Added 'static' + Long id + BookForOneToOne childBook + static hasOne = [childBook: BookForOneToOne] +} + +@Entity +class BookForOneToOne { // Added 'static' + Long id + String title + AuthorWithOneToOne parentAuthor +} +class Address { + String city + String zip +} + +@Entity +class ListAuthor { + Long id + List<ListBook> books + static hasMany = [books: ListBook] +} + +@Entity +class ListBook { + Long id + String title +} + +@Entity +class MapAuthor { + Long id + Map<String, MapBook> books + static hasMany = [books: MapBook] +} + +@Entity +class MapBook { + Long id + String title +} \ No newline at end of file
