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; + } + }
