This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch merge-hibernate6
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit a29d511c88cbf9863ad607dd0385ea3fde5fa787
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Sep 28 21:46:09 2025 -0500

    Major refactoring
---
 .../orm/hibernate/cfg/GrailsDomainBinder.java      | 178 +----
 .../grails/orm/hibernate/cfg/PropertyConfig.groovy |   5 +
 .../hibernate/cfg/domainbinding/ColumnBinder.java  | 142 ++++
 .../ColumnNameForPropertyAndPathFetcher.java       |  92 +++
 .../cfg/domainbinding/CreateKeyForProps.java       |  57 ++
 .../cfg/domainbinding/UserTypeFetcher.java         |  46 ++
 .../cfg/domainbinding/ColumnBinderSpec.groovy      | 761 +++++++++++++++++++++
 .../ColumnNameForPropertyAndPathFetcherSpec.groovy | 138 ++++
 .../cfg/domainbinding/CreateKeyForPropsSpec.groovy | 123 ++++
 .../mapping/model/PersistentProperty.java          |   8 +
 10 files changed, 1379 insertions(+), 171 deletions(-)

diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
index 51ffd13474..dd1a546e57 100644
--- 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
@@ -1085,7 +1085,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
     }
 
     private String getNameForPropertyAndPath(PersistentProperty property, 
String path) {
-        if (isNotEmpty(path)) {
+        if (GrailsHibernateUtil.isNotEmpty(path)) {
             return qualify(path, property.getName());
         }
         return property.getName();
@@ -1533,7 +1533,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         SimpleValue key = new DependantValue(metadataBuildingContext, mytable, 
joinedSubclass.getIdentifier());
         joinedSubclass.setKey(key);
         final PersistentProperty identifier = sub.getIdentity();
-        String columnName = getColumnNameForPropertyAndPath(identifier, 
EMPTY_PATH, null, sessionFactoryBeanName);
+        String columnName = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(identifier,
 EMPTY_PATH, null);
         new SimpleValueColumnBinder().bindSimpleValue(key, 
identifier.getType().getName(), columnName, false);
 
         joinedSubclass.createPrimaryKey();
@@ -1800,7 +1800,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
                 if ("serializable".equals(typeName)) {
                     value = new BasicValue(metadataBuildingContext, table);
                     boolean nullable = currentGrailsProp.isNullable();
-                    String columnName = 
getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null, 
sessionFactoryBeanName);
+                    String columnName = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(currentGrailsProp,
 EMPTY_PATH, null);
                     new 
SimpleValueColumnBinder().bindSimpleValue((SimpleValue) value, typeName, 
columnName, nullable);
                 }
                 else {
@@ -1885,7 +1885,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
     private void bindEnumType(PersistentProperty property, SimpleValue 
simpleValue,
                                 String path, String sessionFactoryBeanName) {
         Class<?> propertyType = property.getType();
-        String columnName = getColumnNameForPropertyAndPath(property, path, 
null, sessionFactoryBeanName);
+        String columnName = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy).getColumnNameForPropertyAndPath(property,
 path, null);
         new EnumTypeBinder().bindEnumType(property, propertyType, simpleValue, 
columnName);
     }
 
@@ -2411,13 +2411,14 @@ public class GrailsDomainBinder implements 
MetadataContributor {
             // in which case we still need to create a Hibernate column for
             // this value.
             var columnConfigToColumnBinder = new ColumnConfigToColumnBinder();
+            var columnBinder = new ColumnBinder(namingStrategy);
             Optional.ofNullable(propertyConfig.getColumns()).
                     filter(list-> !list.isEmpty())
                     .orElse(Arrays.asList(new ColumnConfig[] { null }))
                     .forEach( cc -> {
                         Column column = new Column();
                         
columnConfigToColumnBinder.bindColumnConfigToColumn(column,cc,propertyConfig);
-                        bindColumn(property, parentProperty, column, cc, path, 
table, sessionFactoryBeanName);
+                        columnBinder.bindColumn(property, parentProperty, 
column, cc, path, table);
                         if (table != null) {
                             table.addColumn(column);
                         }
@@ -2426,169 +2427,8 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         }
     }
 
-    /**
-     * Binds a Column instance to the Hibernate meta model
-     *
-     * @param property The Grails domain class property
-     * @param parentProperty
-     * @param column     The column to bind
-     * @param path
-     * @param table      The table name
-     * @param sessionFactoryBeanName  the session factory bean name
-     */
-    private void bindColumn(PersistentProperty property, PersistentProperty 
parentProperty,
-                              Column column, ColumnConfig cc, String path, 
Table table, String sessionFactoryBeanName) {
-
-        if (cc != null) {
-            column.setComment(cc.getComment());
-            column.setDefaultValue(cc.getDefaultValue());
-            column.setCustomRead(cc.getRead());
-            column.setCustomWrite(cc.getWrite());
-        }
-
-        Class<?> userType = getUserType(property);
-        String columnName = getColumnNameForPropertyAndPath(property, path, 
cc, sessionFactoryBeanName);
-        if ((property instanceof Association) && userType == null) {
-            Association association = (Association) property;
-            // Only use conventional naming when the column has not been 
explicitly mapped.
-            if (column.getName() == null) {
-                column.setName(columnName);
-            }
-            if (property instanceof ManyToMany) {
-                column.setNullable(false);
-            }
-            else if (property instanceof 
org.grails.datastore.mapping.model.types.OneToOne && 
association.isBidirectional() && !association.isOwningSide()) {
-                if (((Association) property).getInverseSide().isHasOne()) {
-                    column.setNullable(false);
-                }
-                else {
-                    column.setNullable(true);
-                }
-            }
-            else if ((property instanceof ToOne) && association.isCircular()) {
-                column.setNullable(true);
-            }
-            else {
-                column.setNullable(property.isNullable());
-            }
-        }
-        else {
-            column.setName(columnName);
-            column.setNullable(property.isNullable() || (parentProperty != 
null && parentProperty.isNullable()));
-            PropertyConfig propertyConfig = new 
PersistentPropertyToPropertyConfig().apply(property);
-            // Use the constraints for this property to more accurately define
-            // the column's length, precision, and scale
-            if (String.class.isAssignableFrom(property.getType()) || 
byte[].class.isAssignableFrom(property.getType())) {
-                new 
StringColumnConstraintsBinder().bindStringColumnConstraints(column, 
propertyConfig);
-            }
-
-            if (Number.class.isAssignableFrom(property.getType())) {
-
-                new 
NumericColumnConstraintsBinder().bindNumericColumnConstraints(column, cc, 
propertyConfig);
-            }
-        }
-
-        final PropertyConfig mappedForm = new 
PersistentPropertyToPropertyConfig().apply(property);
-        if (mappedForm.isUnique()) {
-            if (!mappedForm.isUniqueWithinGroup()) {
-                column.setUnique(true);
-            }
-            else {
-                createKeyForProps(property, path, table, columnName, 
mappedForm.getUniquenessGroup(), sessionFactoryBeanName);
-            }
-        }
-
-        new IndexBinder().bindIndex(columnName, column, cc, table);
-
-        final PersistentEntity owner = property.getOwner();
-        if (!owner.isRoot()) {
-            Mapping mapping = new 
HibernateEntityWrapper().getMappedForm(owner);
-            if (mapping == null || mapping.getTablePerHierarchy()) {
-                if (LOG.isDebugEnabled())
-                    LOG.debug("[GrailsDomainBinder] Sub class property [" + 
property.getName() + "] for column name ["+column.getName()+"] set to 
nullable");
-                column.setNullable(true);
-            } else {
-                column.setNullable(property.isNullable());
-            }
-        }
-
-        if (LOG.isDebugEnabled())
-            LOG.debug("[GrailsDomainBinder] bound property [" + 
property.getName() + "] to column name ["+column.getName()+"] in table 
["+table.getName()+"]");
-    }
-
-
-    private void createKeyForProps(PersistentProperty grailsProp, String path, 
Table table,
-                                     String columnName, List<?> propertyNames, 
String sessionFactoryBeanName) {
-        List<Column> keyList = new ArrayList<>();
-        keyList.add(new Column(columnName));
-        for (Iterator<?> i = propertyNames.iterator(); i.hasNext();) {
-            String propertyName = (String) i.next();
-            PersistentProperty otherProp = 
grailsProp.getOwner().getPropertyByName(propertyName);
-            if (otherProp == null) {
-                throw new 
MappingException(grailsProp.getOwner().getJavaClass().getName() + " references 
an unknown property " + propertyName);
-            }
-            String otherColumnName = 
getColumnNameForPropertyAndPath(otherProp, path, null, sessionFactoryBeanName);
-            keyList.add(new Column(otherColumnName));
-        }
-
-        new UniqueKeyForColumnsCreator().createUniqueKeyForColumns(table, 
keyList);
-    }
-
-
-    private String getColumnNameForPropertyAndPath(PersistentProperty 
grailsProp,
-                                                     String path, ColumnConfig 
cc, String sessionFactoryBeanName) {
-        // First try the column config.
-        String columnName = null;
-        if (cc == null) {
-            // No column config given, so try to fetch it from the mapping
-            PersistentEntity domainClass =  grailsProp.getOwner();
-            Mapping m = new 
HibernateEntityWrapper().getMappedForm(domainClass);
-            if (m != null) {
-                PropertyConfig c = m.getPropertyConfig(grailsProp.getName());
-
-                if (supportsJoinColumnMapping(grailsProp) && 
hasJoinKeyMapping(c)) {
-                    columnName = c.getJoinTable().getKey().getName();
-                }
-                else if (c != null && c.getColumn() != null) {
-                    columnName = c.getColumn();
-                }
-            }
-        }
-        else {
-            if (supportsJoinColumnMapping(grailsProp)) {
-                PropertyConfig pc = new 
PersistentPropertyToPropertyConfig().apply(grailsProp);
-                if (hasJoinKeyMapping(pc)) {
-                    columnName = pc.getJoinTable().getKey().getName();
-                }
-                else {
-                    columnName = cc.getName();
-                }
-            }
-            else {
-                columnName = cc.getName();
-            }
-        }
-
-        if (columnName == null) {
-            if (isNotEmpty(path)) {
-                String s1 = getNamingStrategy().resolveColumnName(path);
-
-                String s2 = new 
DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(grailsProp);
-                columnName = new BackticksRemover().apply(s1) + UNDERSCORE + 
new BackticksRemover().apply(s2);
-            } else {
-
-                columnName = new 
DefaultColumnNameFetcher(getNamingStrategy()).getDefaultColumnName(grailsProp);
-            }
-        }
-        return columnName;
-    }
-
     private boolean hasJoinKeyMapping(PropertyConfig c) {
-        return c != null && c.getJoinTable() != null && 
c.getJoinTable().getKey() != null;
-    }
-
-    private boolean supportsJoinColumnMapping(PersistentProperty grailsProp) {
-        return grailsProp instanceof ManyToMany || 
ofNullable(grailsProp).map(PersistentProperty::isUnidirectionalOneToMany).orElse(false)
 || grailsProp instanceof Basic;
+        return c.hasJoinKeyMapping();
     }
 
     private String getIndexColumnName(PersistentProperty property, String 
sessionFactoryBeanName) {
@@ -2622,10 +2462,6 @@ public class GrailsDomainBinder implements 
MetadataContributor {
     }
 
 
-    private boolean isNotEmpty(String s) {
-        return GrailsHibernateUtil.isNotEmpty(s);
-    }
-
     private String qualify(String prefix, String name) {
         return GrailsHibernateUtil.qualify(prefix, name);
     }
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy
index 6dff9f2233..51d353dcf8 100644
--- 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy
@@ -473,4 +473,9 @@ class PropertyConfig extends Property {
         }
         return pc
     }
+
+    public boolean hasJoinKeyMapping() {
+        return  
Optional.ofNullable(getJoinTable()).map(JoinTable::getKey).isPresent();
+    }
+
 }
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinder.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinder.java
new file mode 100644
index 0000000000..574bbe3e1a
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinder.java
@@ -0,0 +1,142 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.Table;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.model.types.ManyToMany;
+import org.grails.datastore.mapping.model.types.OneToOne;
+import org.grails.datastore.mapping.model.types.ToOne;
+import org.grails.orm.hibernate.cfg.ColumnConfig;
+import org.grails.orm.hibernate.cfg.Mapping;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+public class ColumnBinder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ColumnBinder.class);
+
+   private final ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher;
+   private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+   private final StringColumnConstraintsBinder stringColumnConstraintsBinder;
+   private final NumericColumnConstraintsBinder numericColumnConstraintsBinder;
+   private final CreateKeyForProps createKeyForProps;
+   private final HibernateEntityWrapper hibernateEntityWrapper;
+   private final UserTypeFetcher userTypeFetcher;
+   private final IndexBinder indexBinder;
+
+   public ColumnBinder(PersistentEntityNamingStrategy namingStrategy) {
+       this.columnNameForPropertyAndPathFetcher = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy);
+       this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+       this.stringColumnConstraintsBinder = new 
StringColumnConstraintsBinder();
+       this.numericColumnConstraintsBinder = new 
NumericColumnConstraintsBinder();
+       this.createKeyForProps = new 
CreateKeyForProps(columnNameForPropertyAndPathFetcher);
+       this.hibernateEntityWrapper = new HibernateEntityWrapper();
+       this.userTypeFetcher = new UserTypeFetcher();
+       this.indexBinder = new IndexBinder();
+   }
+   protected ColumnBinder(ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher
+   , PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig
+   , StringColumnConstraintsBinder stringColumnConstraintsBinder
+   , NumericColumnConstraintsBinder numericColumnConstraintsBinder
+   , CreateKeyForProps createKeyForProps
+   , HibernateEntityWrapper hibernateEntityWrapper
+   , UserTypeFetcher userTypeFetcher
+   , IndexBinder indexBinder) {
+       this.columnNameForPropertyAndPathFetcher = 
columnNameForPropertyAndPathFetcher;
+       this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+       this.stringColumnConstraintsBinder = stringColumnConstraintsBinder;
+       this.numericColumnConstraintsBinder =  numericColumnConstraintsBinder;
+       this.createKeyForProps = createKeyForProps;
+       this.hibernateEntityWrapper = hibernateEntityWrapper;
+       this.userTypeFetcher = userTypeFetcher;
+       this.indexBinder = indexBinder;
+   }
+    /**
+     * Binds a Column instance to the Hibernate meta model
+     *
+     * @param property       The Grails domain class property
+     * @param parentProperty
+     * @param column         The column to bind
+     * @param path
+     * @param table          The table name
+     */
+    public void bindColumn(PersistentProperty property, PersistentProperty 
parentProperty,
+                           Column column, ColumnConfig cc, String path, Table 
table) {
+
+        if (cc != null) {
+            column.setComment(cc.getComment());
+            column.setDefaultValue(cc.getDefaultValue());
+            column.setCustomRead(cc.getRead());
+            column.setCustomWrite(cc.getWrite());
+        }
+
+        Class<?> userType = userTypeFetcher.getUserType(property);
+        String columnName = 
columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(property, 
path, cc);
+        if ((property instanceof Association association) && userType == null) 
{
+            // Only use conventional naming when the column has not been 
explicitly mapped.
+            if (column.getName() == null) {
+                column.setName(columnName);
+            }
+            if (property instanceof ManyToMany) {
+                column.setNullable(false);
+            }
+            else if (property instanceof OneToOne && 
association.isBidirectional() && !association.isOwningSide()) {
+                if (association.getInverseSide().isHasOne()) {
+                    column.setNullable(false);
+                }
+                else {
+                    column.setNullable(true);
+                }
+            }
+            else if ((property instanceof ToOne) && association.isCircular()) {
+                column.setNullable(true);
+            }
+            else {
+                column.setNullable(property.isNullable());
+            }
+        }
+        else {
+            column.setName(columnName);
+            column.setNullable(property.isNullable() || (parentProperty != 
null && parentProperty.isNullable()));
+            // We'll reuse the same PropertyConfig for any constraints and 
uniqueness
+            PropertyConfig mappedForm = null;
+            // Use the constraints for this property to more accurately define
+            // the column's length, precision, and scale
+            Class<?> type = property.getType();
+            if (type != null && (String.class.isAssignableFrom(type) || 
byte[].class.isAssignableFrom(type))) {
+                if (mappedForm == null) mappedForm = 
persistentPropertyToPropertyConfig.apply(property);
+                
stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm);
+            } else if (type != null && Number.class.isAssignableFrom(type)) {
+                if (mappedForm == null) mappedForm = 
persistentPropertyToPropertyConfig.apply(property);
+                
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, 
mappedForm);
+            }
+        }
+
+        createKeyForProps.createKeyForProps(property, path, table, columnName);
+        indexBinder.bindIndex(columnName, column, cc, table);
+
+        final PersistentEntity owner = property.getOwner();
+        if (!owner.isRoot()) {
+            Mapping mapping = hibernateEntityWrapper.getMappedForm(owner);
+            if (mapping.getTablePerHierarchy()) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug("[GrailsDomainBinder] Sub class property [" + 
property.getName() + "] for column name ["+column.getName()+"] set to 
nullable");
+                column.setNullable(true);
+            } else {
+                column.setNullable(property.isNullable());
+            }
+        }
+
+        // Apply uniqueness last to ensure it isn't overridden by downstream 
binders
+        PropertyConfig mappedFormFinal = 
persistentPropertyToPropertyConfig.apply(property);
+        column.setUnique(mappedFormFinal.isUnique() && 
!mappedFormFinal.isUniqueWithinGroup());
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("[GrailsDomainBinder] bound property [" + 
property.getName() + "] to column name ["+column.getName()+"] in table 
["+table.getName()+"]");
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcher.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcher.java
new file mode 100644
index 0000000000..bab1045a85
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcher.java
@@ -0,0 +1,92 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.orm.hibernate.cfg.ColumnConfig;
+import org.grails.orm.hibernate.cfg.GrailsHibernateUtil;
+import org.grails.orm.hibernate.cfg.Mapping;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+public class ColumnNameForPropertyAndPathFetcher {
+
+    private final PersistentEntityNamingStrategy namingStrategy;
+    private final HibernateEntityWrapper hibernateEntityWrapper;
+    private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+    private final DefaultColumnNameFetcher defaultColumnNameFetcher;
+    private final BackticksRemover backticksRemover;
+
+    public ColumnNameForPropertyAndPathFetcher(PersistentEntityNamingStrategy 
namingStrategy
+    ) {
+        this.namingStrategy = namingStrategy;
+        this.hibernateEntityWrapper = new HibernateEntityWrapper();
+        this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+        this.defaultColumnNameFetcher = new 
DefaultColumnNameFetcher(namingStrategy);
+        this.backticksRemover = new BackticksRemover();
+    }
+
+    protected 
ColumnNameForPropertyAndPathFetcher(PersistentEntityNamingStrategy 
namingStrategy
+            , HibernateEntityWrapper hibernateEntityWrapper
+            , PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig
+    , DefaultColumnNameFetcher defaultColumnNameFetcher
+    , BackticksRemover backticksRemover) {
+        this.namingStrategy = namingStrategy;
+        this.hibernateEntityWrapper = hibernateEntityWrapper;
+        this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+        this.defaultColumnNameFetcher = defaultColumnNameFetcher;
+        this.backticksRemover = backticksRemover;
+
+    }
+
+
+    private static final String UNDERSCORE = "_";
+
+    public String getColumnNameForPropertyAndPath(PersistentProperty 
grailsProp,
+                                                   String path, ColumnConfig 
cc) {
+        // First try the column config.
+        String columnName = null;
+        if (cc == null) {
+            // No column config given, attempt to obtain the property config 
directly from the property
+            PropertyConfig c = null;
+            try {
+                c = persistentPropertyToPropertyConfig.apply(grailsProp);
+            } catch (Exception ignore) {
+                // If we cannot resolve a PropertyConfig, treat as absent and 
fall back later
+            }
+
+            if (grailsProp.supportsJoinColumnMapping() && c != null && 
c.hasJoinKeyMapping()) {
+                columnName = c.getJoinTable().getKey().getName();
+            }
+            else if (c != null && c.getColumn() != null) {
+                columnName = c.getColumn();
+            }
+        }
+        else {
+            if (grailsProp.supportsJoinColumnMapping()) {
+                PropertyConfig pc = 
persistentPropertyToPropertyConfig.apply(grailsProp);
+                if (pc.hasJoinKeyMapping()) {
+                    columnName = pc.getJoinTable().getKey().getName();
+                }
+                else {
+                    columnName = cc.getName();
+                }
+            }
+            else {
+                columnName = cc.getName();
+            }
+        }
+
+        if (columnName == null) {
+            if (GrailsHibernateUtil.isNotEmpty(path)) {
+                String s1 = namingStrategy.resolveColumnName(path);
+
+                String s2 = 
defaultColumnNameFetcher.getDefaultColumnName(grailsProp);
+                columnName = backticksRemover.apply(s1) + UNDERSCORE + 
backticksRemover.apply(s2);
+            } else {
+
+                columnName = 
defaultColumnNameFetcher.getDefaultColumnName(grailsProp);
+            }
+        }
+        return columnName;
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForProps.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForProps.java
new file mode 100644
index 0000000000..5eff359cd3
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForProps.java
@@ -0,0 +1,57 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.hibernate.MappingException;
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.Table;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+public class CreateKeyForProps {
+
+    private final ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher;
+    private final UniqueKeyForColumnsCreator uniqueKeyForColumnsCreator;
+    private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+
+    public CreateKeyForProps(ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher) {
+        this.columnNameForPropertyAndPathFetcher = 
columnNameForPropertyAndPathFetcher;
+        this.uniqueKeyForColumnsCreator = new UniqueKeyForColumnsCreator();
+        this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+    }
+    protected CreateKeyForProps(ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher
+            , UniqueKeyForColumnsCreator uniqueKeyForColumnsCreator
+    , PersistentPropertyToPropertyConfig persistentPropertyToPropertyConfig) {
+        this.columnNameForPropertyAndPathFetcher = 
columnNameForPropertyAndPathFetcher;
+        this.uniqueKeyForColumnsCreator = uniqueKeyForColumnsCreator;
+        this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+    }
+
+    public void createKeyForProps(PersistentProperty grailsProp, String path, 
Table table,
+                                   String columnName) {
+        PropertyConfig mappedForm = 
persistentPropertyToPropertyConfig.apply(grailsProp);
+
+        if (mappedForm.isUnique() && mappedForm.isUniqueWithinGroup()) {
+
+            List<Column> keyList = new ArrayList<>();
+            keyList.add(new Column(columnName));
+            List<String> propertyNames = mappedForm.getUniquenessGroup();
+            PersistentEntity owner = grailsProp.getOwner();
+            for (Iterator<?> i = propertyNames.iterator(); i.hasNext();) {
+                String propertyName = (String) i.next();
+                PersistentProperty otherProp = 
owner.getPropertyByName(propertyName);
+                if (otherProp == null) {
+                    throw new MappingException(owner.getJavaClass().getName() 
+ " references an unknown property " + propertyName);
+                }
+                String otherColumnName = 
columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(otherProp, 
path, null);
+                keyList.add(new Column(otherColumnName));
+            }
+
+            uniqueKeyForColumnsCreator.createUniqueKeyForColumns(table, 
keyList);
+        }
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UserTypeFetcher.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UserTypeFetcher.java
new file mode 100644
index 0000000000..cc0f6159fb
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UserTypeFetcher.java
@@ -0,0 +1,46 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.orm.hibernate.cfg.GrailsDomainBinder;
+import org.grails.orm.hibernate.cfg.PropertyConfig;
+
+public class UserTypeFetcher {
+
+    private final PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig;
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(UserTypeFetcher.class);
+
+
+    public UserTypeFetcher() {
+        this.persistentPropertyToPropertyConfig = new 
PersistentPropertyToPropertyConfig();
+    }
+
+    protected UserTypeFetcher(PersistentPropertyToPropertyConfig 
persistentPropertyToPropertyConfig) {
+        this.persistentPropertyToPropertyConfig = 
persistentPropertyToPropertyConfig;
+    }
+    public Class<?> getUserType(PersistentProperty currentGrailsProp) {
+        Class<?> userType = null;
+        PropertyConfig config = 
persistentPropertyToPropertyConfig.apply(currentGrailsProp);
+        Object typeObj = config == null ? null : config.getType();
+        if (typeObj instanceof Class<?>) {
+            userType = (Class<?>)typeObj;
+        } else if (typeObj != null) {
+            String typeName = typeObj.toString();
+            try {
+                userType = Class.forName(typeName, true, 
Thread.currentThread().getContextClassLoader());
+            } catch (ClassNotFoundException e) {
+                // only print a warning if the user type is in a package this 
excludes basic
+                // types like string, int etc.
+                if (typeName.indexOf(".")>-1) {
+                    if (LOG.isWarnEnabled()) {
+                        LOG.warn("UserType not found ", e);
+                    }
+                }
+            }
+        }
+        return userType;
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
new file mode 100644
index 0000000000..2aadc19de0
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
@@ -0,0 +1,761 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.ManyToMany
+import org.grails.orm.hibernate.cfg.ColumnConfig
+import org.grails.orm.hibernate.cfg.Mapping
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
+import org.grails.orm.hibernate.cfg.PropertyConfig
+import org.hibernate.mapping.Column
+import org.hibernate.mapping.Table
+import spock.lang.Specification
+
+class ColumnBinderSpec extends Specification {
+
+    def "association ManyToMany without userType uses fetched name and is not 
nullable"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(ManyToMany)
+        def owner = Mock(PersistentEntity)
+        def mappedForm = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        // stubs
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"mtm_fk"
+        prop.isNullable() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> true // skip subclass nullable logic
+        propToConfig.apply(prop) >> mappedForm
+        mappedForm.isUnique() >> false
+        mappedForm.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "mtm_fk"
+        column.isNullable() == false
+        0 * stringBinder._
+        0 * numericBinder._
+        1 * keyCreator.createKeyForProps(prop, null, table, "mtm_fk")
+        1 * indexBinder.bindIndex("mtm_fk", column, null, table)
+    }
+
+    def "numeric non-association property applies config, numeric constraints, 
unique and subclass TPH nullable"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def parentProp = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+        def cc = new ColumnConfig(comment: "cmt", defaultValue: "def", read: 
"r", write: "w")
+
+        // stubs
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, "p", cc) >> 
"num_col"
+        prop.getType() >> Integer
+        prop.isNullable() >> false
+        parentProp.isNullable() >> true // should make column initially 
nullable
+        prop.getOwner() >> owner
+        owner.isRoot() >> false
+        hibernateWrapper.getMappedForm(owner) >> mapping
+        mapping.getTablePerHierarchy() >> true // forces nullable true for 
subclass
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig] // 
called twice in code
+        // numeric constraints applied
+        // unique settings
+        propertyConfig.isUnique() >> true
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, parentProp, column, cc, "p", table)
+
+        then:
+        column.getName() == "num_col"
+        column.isNullable() == true // due to subclass TPH logic
+        column.getComment() == "cmt"
+        column.getDefaultValue() == "def"
+        column.getCustomRead() == "r"
+        column.getCustomWrite() == "w"
+
+        1 * numericBinder.bindNumericColumnConstraints(column, cc, 
propertyConfig)
+        0 * stringBinder._
+        1 * keyCreator.createKeyForProps(prop, "p", table, "num_col")
+        1 * indexBinder.bindIndex("num_col", column, cc, table)
+    }
+
+    def "one-to-one inverse non-owning with hasOne keeps existing name and 
sets nullable=false"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(org.grails.datastore.mapping.model.types.OneToOne)
+        def inverse = 
Mock(org.grails.datastore.mapping.model.types.Association)
+        def owner = Mock(PersistentEntity)
+        def mappedForm = Mock(PropertyConfig)
+        def column = new Column("pre_existing")
+        def table = new Table()
+
+        // stubs
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"fetched_col"
+        prop.isNullable() >> true
+        prop.isBidirectional() >> true
+        prop.isOwningSide() >> false
+        prop.getInverseSide() >> inverse
+        inverse.isHasOne() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >> mappedForm
+        mappedForm.isUnique() >> false
+        mappedForm.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "pre_existing" // should not overwrite existing 
name
+        column.isNullable() == false
+        1 * keyCreator.createKeyForProps(prop, null, table, "fetched_col")
+        1 * indexBinder.bindIndex("fetched_col", column, null, table)
+        0 * stringBinder._
+        0 * numericBinder._
+    }
+
+    def "string property triggers string constraints binder only"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"str_col"
+        prop.getType() >> String
+        prop.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig]
+        propertyConfig.isUnique() >> false
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "str_col"
+        column.isNullable()
+        1 * stringBinder.bindStringColumnConstraints(column, propertyConfig)
+        0 * numericBinder._
+        1 * keyCreator.createKeyForProps(prop, null, table, "str_col")
+        1 * indexBinder.bindIndex("str_col", column, null, table)
+    }
+
+    def "one-to-one inverse non-owning without hasOne sets nullable=true"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(org.grails.datastore.mapping.model.types.OneToOne)
+        def inverse = 
Mock(org.grails.datastore.mapping.model.types.Association)
+        def owner = Mock(PersistentEntity)
+        def mappedForm = Mock(PropertyConfig)
+        def column = new Column() // name is null so binder should set it
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"one_to_one_fk"
+        prop.isNullable() >> false // but branch should override to true due 
to !hasOne
+        prop.isBidirectional() >> true
+        prop.isOwningSide() >> false
+        prop.getInverseSide() >> inverse
+        inverse.isHasOne() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >> mappedForm
+        mappedForm.isUnique() >> false
+        mappedForm.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "one_to_one_fk"
+        column.isNullable() == true
+        1 * keyCreator.createKeyForProps(prop, null, table, "one_to_one_fk")
+        1 * indexBinder.bindIndex("one_to_one_fk", column, null, table)
+        0 * stringBinder._
+        0 * numericBinder._
+    }
+
+    def "to-one circular association sets nullable=true"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(org.grails.datastore.mapping.model.types.ToOne)
+        def owner = Mock(PersistentEntity)
+        def mappedForm = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"to_one_fk"
+        prop.isCircular() >> true
+        prop.isNullable() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >> mappedForm
+        mappedForm.isUnique() >> false
+        mappedForm.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "to_one_fk"
+        column.isNullable() == true
+        1 * keyCreator.createKeyForProps(prop, null, table, "to_one_fk")
+        1 * indexBinder.bindIndex("to_one_fk", column, null, table)
+        0 * stringBinder._
+        0 * numericBinder._
+    }
+
+    def "association default nullable falls back to property.isNullable()"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(org.grails.datastore.mapping.model.types.Association)
+        def owner = Mock(PersistentEntity)
+        def mappedForm = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"assoc_fk"
+        prop.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        // ensure we don't hit special branches
+        // Spock will not have methods isBidirectional, isOwningSide on 
Association base; not needed since code checks only if OneToOne or 
ToOne/circular
+        propToConfig.apply(prop) >> mappedForm
+        mappedForm.isUnique() >> false
+        mappedForm.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "assoc_fk"
+        column.isNullable() == true
+        1 * keyCreator.createKeyForProps(prop, null, table, "assoc_fk")
+        1 * indexBinder.bindIndex("assoc_fk", column, null, table)
+        0 * stringBinder._
+        0 * numericBinder._
+    }
+
+    def "non-association nullable computed as property OR parent (prop=true, 
parent=false)"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def parentProp = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"na_col"
+        prop.isNullable() >> true
+        parentProp.isNullable() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig]
+        propertyConfig.isUnique() >> false
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, parentProp, column, null, null, table)
+
+        then:
+        column.getName() == "na_col"
+        column.isNullable() == true
+        0 * stringBinder._
+        0 * numericBinder._
+        1 * keyCreator.createKeyForProps(prop, null, table, "na_col")
+        1 * indexBinder.bindIndex("na_col", column, null, table)
+    }
+
+    def "non-association nullable computed as property OR parent (prop=false, 
parent=true)"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def parentProp = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"na_col2"
+        prop.isNullable() >> false
+        parentProp.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig]
+        propertyConfig.isUnique() >> false
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, parentProp, column, null, null, table)
+
+        then:
+        column.getName() == "na_col2"
+        column.isNullable() == true
+        1 * keyCreator.createKeyForProps(prop, null, table, "na_col2")
+        1 * indexBinder.bindIndex("na_col2", column, null, table)
+    }
+
+    def "non-association nullable computed as property OR parent (both 
false)"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def parentProp = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"na_col3"
+        prop.isNullable() >> false
+        parentProp.isNullable() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig]
+        propertyConfig.isUnique() >> false
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, parentProp, column, null, null, table)
+
+        then:
+        column.getName() == "na_col3"
+        column.isNullable() == false
+        1 * keyCreator.createKeyForProps(prop, null, table, "na_col3")
+        1 * indexBinder.bindIndex("na_col3", column, null, table)
+    }
+
+    def "uniqueness handling scenarios 1"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"u_col"
+        prop.getType() >> Object
+        prop.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+
+        when:
+        // Unique true, withinGroup false => unique true
+        def pc1 = Mock(PropertyConfig)
+        propToConfig.apply(prop) >>> [pc1, pc1]
+        pc1.isUnique() >> true
+        pc1.isUniqueWithinGroup() >> false
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.isUnique()
+
+    }
+
+    def "uniqueness handling scenarios 2"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"u_col"
+        prop.getType() >> Object
+        prop.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+
+
+        when:
+        // Unique true, withinGroup true => unique false
+        def column2 = new Column()
+        def pc2 = Mock(PropertyConfig)
+        propToConfig.apply(prop) >> pc2
+        pc2.isUnique() >> true
+        pc2.isUniqueWithinGroup() >> true
+        binder.bindColumn(prop, null, column2, null, null, table)
+
+        then:
+        !column2.isUnique()
+
+    }
+
+    def "uniqueness handling scenarios 3"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"u_col"
+        prop.getType() >> Object
+        prop.isNullable() >> true
+        prop.getOwner() >> owner
+        owner.isRoot() >> true
+
+
+        when:
+        // Unique false => unique false
+        def column3 = new Column()
+        def pc3 = Mock(PropertyConfig)
+        propToConfig.apply(prop) >>> [pc3, pc3]
+        pc3.isUnique() >> false
+        pc3.isUniqueWithinGroup() >> false
+        binder.bindColumn(prop, null, column3, null, null, table)
+
+        then:
+        !column3.isUnique()
+    }
+
+    def "owner not root with tablePerHierarchy=false sets nullable to 
property.isNullable()"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def stringBinder = Mock(StringColumnConstraintsBinder)
+        def numericBinder = Mock(NumericColumnConstraintsBinder)
+        def keyCreator = Mock(CreateKeyForProps)
+        def hibernateWrapper = Mock(HibernateEntityWrapper)
+        def userTypeFetcher = Mock(UserTypeFetcher)
+        def indexBinder = Mock(IndexBinder)
+
+        def binder = new ColumnBinder(
+
+                columnNameFetcher,
+                propToConfig,
+                stringBinder,
+                numericBinder,
+                keyCreator,
+                hibernateWrapper,
+                userTypeFetcher,
+                indexBinder
+        )
+
+        def prop = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def propertyConfig = Mock(PropertyConfig)
+        def column = new Column()
+        def table = new Table()
+
+        userTypeFetcher.getUserType(prop) >> null
+        columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> 
"sub_col"
+        prop.getType() >> Object
+        prop.isNullable() >> false
+        prop.getOwner() >> owner
+        owner.isRoot() >> false
+        hibernateWrapper.getMappedForm(owner) >> mapping
+        mapping.getTablePerHierarchy() >> false
+        propToConfig.apply(prop) >>> [propertyConfig, propertyConfig]
+        propertyConfig.isUnique() >> false
+        propertyConfig.isUniqueWithinGroup() >> false
+
+        when:
+        binder.bindColumn(prop, null, column, null, null, table)
+
+        then:
+        column.getName() == "sub_col"
+        !column.isNullable()
+        1 * keyCreator.createKeyForProps(prop, null, table, "sub_col")
+        1 * indexBinder.bindIndex("sub_col", column, null, table)
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcherSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcherSpec.groovy
new file mode 100644
index 0000000000..5b29b62f4f
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnNameForPropertyAndPathFetcherSpec.groovy
@@ -0,0 +1,138 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.orm.hibernate.cfg.ColumnConfig
+import org.grails.orm.hibernate.cfg.Mapping
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
+import org.grails.orm.hibernate.cfg.PropertyConfig
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class ColumnNameForPropertyAndPathFetcherSpec extends Specification {
+
+    def backticksRemover = new BackticksRemover()
+
+    def "when ColumnConfig is null and mapping has explicit column then it is 
used"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def hibernateEntityWrapper = Mock(HibernateEntityWrapper)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def defaultColumnFetcher = Mock(DefaultColumnNameFetcher)
+        def fetcher = new ColumnNameForPropertyAndPathFetcher(
+                namingStrategy,
+                hibernateEntityWrapper,
+                propToConfig,
+                defaultColumnFetcher,
+                backticksRemover
+        )
+
+        def grailsProp = Mock(PersistentProperty)
+        def owner = Mock(PersistentEntity)
+        def mapping = Mock(Mapping)
+        def pc = Mock(PropertyConfig)
+
+        grailsProp.supportsJoinColumnMapping() >> false
+        propToConfig.apply(grailsProp) >> pc
+        pc.getColumn() >> "explicit_col"
+
+        when:
+        def result = fetcher.getColumnNameForPropertyAndPath(grailsProp, null, 
null)
+
+        then:
+        result == "explicit_col"
+    }
+
+    def "when ColumnConfig provided and join key mapping exists then join key 
name is used"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def hibernateEntityWrapper = Mock(HibernateEntityWrapper)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def defaultColumnFetcher = Mock(DefaultColumnNameFetcher)
+        def fetcher = new ColumnNameForPropertyAndPathFetcher(
+                namingStrategy,
+                hibernateEntityWrapper,
+                propToConfig,
+                defaultColumnFetcher,
+                backticksRemover
+        )
+
+        def grailsProp = Mock(PersistentProperty)
+        def providedColumn = new ColumnConfig(name: "ignored_when_join_key")
+        def pc = Mock(PropertyConfig)
+        def joinTable = Mock(org.grails.orm.hibernate.cfg.JoinTable)
+        def key = new ColumnConfig(name: "join_key_name")
+
+        grailsProp.supportsJoinColumnMapping() >> true
+        propToConfig.apply(grailsProp) >> pc
+        pc.hasJoinKeyMapping() >> true
+        pc.getJoinTable() >> joinTable
+        joinTable.getKey() >> key
+
+        when:
+        def result = fetcher.getColumnNameForPropertyAndPath(grailsProp, null, 
providedColumn)
+
+        then:
+        result == "join_key_name"
+    }
+
+    @Unroll
+    def "when no explicit column then builds from path '#path' and default 
column '#defaultCol' with backticks removed"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def hibernateEntityWrapper = Mock(HibernateEntityWrapper)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def defaultColumnFetcher = Mock(DefaultColumnNameFetcher)
+        def fetcher = new ColumnNameForPropertyAndPathFetcher(
+                namingStrategy,
+                hibernateEntityWrapper,
+                propToConfig,
+                defaultColumnFetcher,
+                backticksRemover
+        )
+
+        def grailsProp = Mock(PersistentProperty)
+
+        // No config available and no join mapping path
+        grailsProp.supportsJoinColumnMapping() >> false
+
+        namingStrategy.resolveColumnName(path) >> resolvedPath
+        defaultColumnFetcher.getDefaultColumnName(grailsProp) >> defaultCol
+
+        when:
+        def result = fetcher.getColumnNameForPropertyAndPath(grailsProp, path, 
null)
+
+        then:
+        result == expected
+
+        where:
+        path              | resolvedPath       | defaultCol           || 
expected
+        "`order`"        | "`order`"          | "`customer_id`"      || 
"order_customer_id"
+        "invoice"        | "invoice"          | "line_item_id"       || 
"invoice_line_item_id"
+    }
+
+    def "when path is empty falls back to default column name only"() {
+        given:
+        def namingStrategy = Mock(PersistentEntityNamingStrategy)
+        def hibernateEntityWrapper = Mock(HibernateEntityWrapper)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def defaultColumnFetcher = Mock(DefaultColumnNameFetcher)
+        def fetcher = new ColumnNameForPropertyAndPathFetcher(
+                namingStrategy,
+                hibernateEntityWrapper,
+                propToConfig,
+                defaultColumnFetcher,
+                backticksRemover
+        )
+
+        def grailsProp = Mock(PersistentProperty)
+
+        defaultColumnFetcher.getDefaultColumnName(grailsProp) >> "only_default"
+
+        when:
+        def result = fetcher.getColumnNameForPropertyAndPath(grailsProp, null, 
null)
+
+        then:
+        result == "only_default"
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForPropsSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForPropsSpec.groovy
new file mode 100644
index 0000000000..38e5d6dee8
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CreateKeyForPropsSpec.groovy
@@ -0,0 +1,123 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.hibernate.MappingException
+import org.hibernate.mapping.Table
+import spock.lang.Specification
+
+class CreateKeyForPropsSpec extends Specification {
+
+    def "creates unique key when property is unique within group"() {
+        given:
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def uniqueKeyCreator = Mock(UniqueKeyForColumnsCreator)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def subject = new CreateKeyForProps(columnNameFetcher, 
uniqueKeyCreator, propToConfig)
+
+        def owner = Mock(PersistentEntity)
+        def grailsProp = Mock(PersistentProperty) {
+            getOwner() >> owner
+        }
+
+        def mappedForm = Mock(org.grails.orm.hibernate.cfg.PropertyConfig) {
+            isUnique() >> true
+            isUniqueWithinGroup() >> true
+            getUniquenessGroup() >> ["p1", "p2"]
+        }
+        propToConfig.apply(grailsProp) >> mappedForm
+
+        def otherProp1 = Mock(PersistentProperty)
+        def otherProp2 = Mock(PersistentProperty)
+        owner.getPropertyByName("p1") >> otherProp1
+        owner.getPropertyByName("p2") >> otherProp2
+
+        String path = "some_path"
+        def table = new Table("t")
+        String baseColumnName = "base_col"
+
+        columnNameFetcher.getColumnNameForPropertyAndPath(otherProp1, path, 
null) >> "col1"
+        columnNameFetcher.getColumnNameForPropertyAndPath(otherProp2, path, 
null) >> "col2"
+
+        when:
+        subject.createKeyForProps(grailsProp, path, table, baseColumnName)
+
+        then:
+        1 * propToConfig.apply(grailsProp) >> mappedForm
+        1 * grailsProp.getOwner() >> owner
+        1 * mappedForm.isUnique() >> true
+        1 * mappedForm.isUniqueWithinGroup() >> true
+        1 * mappedForm.getUniquenessGroup() >> ["p1", "p2"]
+        1 * owner.getPropertyByName("p1") >> otherProp1
+        1 * owner.getPropertyByName("p2") >> otherProp2
+        1 * columnNameFetcher.getColumnNameForPropertyAndPath(otherProp1, 
path, null)
+        1 * columnNameFetcher.getColumnNameForPropertyAndPath(otherProp2, 
path, null)
+        1 * uniqueKeyCreator.createUniqueKeyForColumns(table, _ as List)
+    }
+
+    def "does nothing when property is not unique within group"() {
+        given:
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def uniqueKeyCreator = Mock(UniqueKeyForColumnsCreator)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def subject = new CreateKeyForProps(columnNameFetcher, 
uniqueKeyCreator, propToConfig)
+
+        def owner = Mock(PersistentEntity)
+        def grailsProp = Mock(PersistentProperty) { getOwner() >> owner }
+
+        def mappedForm = Mock(org.grails.orm.hibernate.cfg.PropertyConfig) {
+            isUnique() >> false
+            isUniqueWithinGroup() >> true
+            getUniquenessGroup() >> ["p1"]
+        }
+        propToConfig.apply(grailsProp) >> mappedForm
+
+        when:
+        subject.createKeyForProps(grailsProp, null, new Table("t"), "base")
+
+        then:
+        1 * propToConfig.apply(grailsProp) >> mappedForm
+        0 * grailsProp.getOwner() >> owner
+        1 * mappedForm.isUnique() >> false
+        0 * uniqueKeyCreator._
+        0 * columnNameFetcher._
+        0 * _
+    }
+
+    def "throws when uniqueness group references unknown property"() {
+        given:
+        def columnNameFetcher = Mock(ColumnNameForPropertyAndPathFetcher)
+        def uniqueKeyCreator = Mock(UniqueKeyForColumnsCreator)
+        def propToConfig = Mock(PersistentPropertyToPropertyConfig)
+        def subject = new CreateKeyForProps(columnNameFetcher, 
uniqueKeyCreator, propToConfig)
+
+        def owner = Mock(PersistentEntity)
+        def grailsProp = Mock(PersistentProperty) { getOwner() >> owner }
+        owner.getJavaClass() >> CreateKeyForPropsSpec
+
+        def mappedForm = Mock(org.grails.orm.hibernate.cfg.PropertyConfig) {
+            isUnique() >> true
+            isUniqueWithinGroup() >> true
+            getUniquenessGroup() >> ["missingProp"]
+        }
+        propToConfig.apply(grailsProp) >> mappedForm
+
+        owner.getPropertyByName("missingProp") >> null
+
+        when:
+        subject.createKeyForProps(grailsProp, null, new Table("t"), "base")
+
+        then:
+        thrown(MappingException)
+        1 * propToConfig.apply(grailsProp) >> mappedForm
+        1 * grailsProp.getOwner() >> owner
+        1 * mappedForm.isUnique() >> true
+        1 * mappedForm.isUniqueWithinGroup() >> true
+        1 * mappedForm.getUniquenessGroup() >> ["missingProp"]
+        1 * owner.getJavaClass() >> CreateKeyForPropsSpec
+        1 * owner.getPropertyByName("missingProp")
+        0 * uniqueKeyCreator._
+        0 * columnNameFetcher._
+        0 * _
+    }
+}
diff --git 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java
 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java
index fff567779a..d6f3a30551 100644
--- 
a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java
+++ 
b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java
@@ -21,7 +21,9 @@ package org.grails.datastore.mapping.model;
 
 import org.grails.datastore.mapping.config.Property;
 import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.model.types.Basic;
 import org.grails.datastore.mapping.model.types.Embedded;
+import org.grails.datastore.mapping.model.types.ManyToMany;
 import org.grails.datastore.mapping.model.types.ManyToOne;
 import org.grails.datastore.mapping.model.types.OneToMany;
 import org.grails.datastore.mapping.model.types.ToOne;
@@ -29,6 +31,8 @@ import org.grails.datastore.mapping.reflect.EntityReflector;
 
 import java.util.Optional;
 
+import static java.util.Optional.ofNullable;
+
 /**
  * @author Graeme Rocher
  * @since 1.0
@@ -112,4 +116,8 @@ public interface PersistentProperty<T extends Property> {
         return false;
     }
 
+    default boolean supportsJoinColumnMapping() {
+        return this instanceof ManyToMany || isUnidirectionalOneToMany()|| 
this instanceof Basic;
+    }
+
 }

Reply via email to