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

Reply via email to