This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit a10ab1f03fb04aa2b0a47ec21bf01951bb5a32b4 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Mar 19 08:27:29 2026 -0500 hibernate 7: ### Mapping Closure Evaluation Bug (Fixed) **Issue:** In `HibernateMappingBuilder.groovy`, the `index` property was being aggressively converted to a `String` via `.toString()`. When a closure was provided (e.g., `index: { column 'foo' }`), the internal Groovy closure class name was stored instead of the closure itself, preventing proper evaluation during the second pass. **Action:** Removed the premature `.toString()` conversion in `HibernateMappingBuilder.handlePropertyInternal`. ### Named Unique Constraints Support (Implemented) **Issue:** During the exploration of `ColumnConfig` possibilities, it was noted that the `unique` property was restricted to `boolean`, preventing the use of named unique constraints (uniqueness groups) via the DSL. **Action:** Transitioned `unique` to `Object` in `ColumnConfig.groovy` and added an `isUnique()` helper for Java compatibility. Updated `HibernateMappingBuilder` and `ColumnConfigToColumnBinder` to handle the flexible type. This now allows configuration like `unique: 'groupName'` or `unique: ['group1', 'group2']` through the mapping DSL. ### Multi-Column Property Re-evaluation Bug (Fixed) **Issue:** Found a bug in `PropertyDefinitionDelegate` where re-evaluating a property with multiple columns would always overwrite the first column instead of correctly updating subsequent ones. **Action:** Fixed `PropertyDefinitionDelegate` to use the current `index` when accessing existing `ColumnConfig` objects. Added `PropertyDefinitionDelegateSpec` to verify the fix. --- .../grails/orm/hibernate/cfg/ColumnConfig.groovy | 82 +- .../cfg/PropertyDefinitionDelegate.groovy | 4 +- .../binder/ColumnConfigToColumnBinder.java | 2 +- .../hibernate/HibernateMappingBuilder.groovy | 4 +- .../hibernate/HibernateToManyProperty.java | 98 ++- .../secondpass/CollectionSecondPassBinder.java | 59 +- .../domainbinding/util/CascadeBehaviorFetcher.java | 95 ++- .../mapping/HibernateMappingBuilderSpec.groovy | 458 +++++++++++ .../mapping/HibernateMappingBuilderTests.groovy | 902 --------------------- .../gorm/specs/HibernateGormDatastoreSpec.groovy | 13 +- .../orm/hibernate/cfg/ColumnConfigSpec.groovy | 155 ++++ .../cfg/PropertyDefinitionDelegateSpec.groovy | 61 ++ .../CascadeBehaviorFetcherSpec.groovy | 258 +++--- .../ColumnConfigToColumnBinderSpec.groovy | 48 ++ .../hibernate/HibernateToManyPropertySpec.groovy | 131 ++- .../CollectionSecondPassBinderSpec.groovy | 48 +- .../secondpass/MapSecondPassBinderSpec.groovy | 38 +- 17 files changed, 1314 insertions(+), 1142 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy index 8f2b92c3f0..5cdae991d7 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy @@ -49,10 +49,90 @@ class ColumnConfig { * The index, can be either a boolean or a string for the name of the index */ def index + + /** + * Parses the index field when stored as a Groovy-style string literal. + * Expected format: [column:item_idx, type:integer] or column:item_idx, type:integer + * Returns an empty map if parsing fails or the value is invalid. + * Throws IllegalArgumentException only if the format is clearly broken (fail-fast for bad developer input). + */ + Map<String, String> getIndexAsMap() { + Object raw = this.index + if (raw == null) return [:] + + if (raw instanceof Map) { + // Already a map → return as-is (though unlikely) + return raw.collectEntries { k, v -> [k.toString(), v.toString()] } as Map<String, String> + } + + if (!(raw instanceof String)) { + // If it's a closure or something else, we can't parse it as a string map. + // Let the caller handle other types (like closures). + return [:] + } + String rawStr = raw.toString() + + String content = rawStr.trim() + + // Remove surrounding [ ] if present + if (content.startsWith('[') && content.endsWith(']')) { + content = content.substring(1, content.length() - 1).trim() + } + + if (!content) return [:] + + Map<String, String> result = [:] + + // Split on top-level commas (simple heuristic: assume no commas inside values) + content.split(',').each { pair -> + def trimmed = pair.trim() + if (!trimmed) return + + def kv = trimmed.split(':', 2) + if (kv.length != 2) { + // If it's the only pair and doesn't have a colon, treat it as the column name + if (content == trimmed && !content.contains(',')) { + result['column'] = content + return + } + // Invalid pair → fail fast (developer mistake) + throw new IllegalArgumentException( + "Invalid index pair format '$trimmed' in string: '$raw'" + ) + } + + String key = kv[0].trim() + String value = kv[1].trim() + + // Strip surrounding quotes from value if present + if ((value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"'))) { + value = value.substring(1, value.length() - 1) + } + + result[key] = value + } + + if (result.isEmpty()) { + throw new IllegalArgumentException("No valid key:value pairs found in index string: '$raw'") + } + + return result + } /** * Whether the column is unique */ - boolean unique = false + def unique = false + + /** + * @return Whether the column is unique + */ + boolean isUnique() { + if (unique instanceof Boolean) { + return (Boolean) unique + } + return unique != null && unique != false + } /** * The length of the column */ diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy index 6a0de90938..275c4513a4 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy @@ -54,7 +54,7 @@ class PropertyDefinitionDelegate { ColumnConfig column if (index < config.columns.size()) { // configure existing - column = config.columns[0] + column = config.columns[index] } else { column = new ColumnConfig() @@ -65,7 +65,7 @@ class PropertyDefinitionDelegate { column.sqlType = args['sqlType'] column.enumType = args['enumType'] ?: column.enumType column.index = args['index'] - column.unique = args['unique'] ?: false + column.unique = args['unique'] != null ? args['unique'] : false column.length = args['length'] ? args['length'] as Integer : -1 column.precision = args['precision'] ? args['precision'] as Integer : -1 column.scale = args['scale'] ? args['scale'] as Integer : -1 diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java index 9a9a6757be..af4704fe2b 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java @@ -41,7 +41,7 @@ public class ColumnConfigToColumnBinder { Optional.ofNullable(mappedForm) .filter(mf -> !mf.isUniqueWithinGroup()) - .ifPresent(mf -> column.setUnique(config.getUnique())); + .ifPresent(mf -> column.setUnique(config.isUnique())); }); } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy index 632cec0fe5..ba1ea1fdae 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy @@ -366,9 +366,9 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr Object enumTypeVal = namedArgs['enumType'] if (enumTypeVal) cc.enumType = enumTypeVal.toString() Object indexVal = namedArgs['index'] - if (indexVal) cc.index = indexVal.toString() + if (indexVal) cc.index = indexVal Object ccUniqueVal = namedArgs['unique'] - if (ccUniqueVal) cc.unique = ccUniqueVal instanceof Boolean ? (Boolean) ccUniqueVal : ccUniqueVal + if (ccUniqueVal != null) cc.unique = ccUniqueVal Object readVal = namedArgs['read'] if (readVal) cc.read = readVal.toString() Object writeVal = namedArgs['write'] diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java index 20bdb67177..2ac3f2c971 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java @@ -97,20 +97,86 @@ public interface HibernateToManyProperty extends PropertyWithMapping<PropertyCon && !(this instanceof Basic); } - default String getIndexColumnType(String defaultType) { - return java.util.Optional.ofNullable(getMappedForm()) - .map(PropertyConfig::getIndexColumn) - .map(ic -> getTypeName(ic, getHibernateOwner().getMappedForm())) - .orElse(defaultType); + default String getIndexColumnName(PersistentEntityNamingStrategy namingStrategy) { + PropertyConfig mapped = getMappedForm(); + + if (mapped != null && mapped.getIndexColumn() != null) { + PropertyConfig indexColConfig = mapped.getIndexColumn(); + if (!indexColConfig.getColumns().isEmpty()) { + String name = indexColConfig.getColumns().get(0).getName(); + if (StringUtils.hasText(name)) { + return name; + } + } + } + + if (mapped == null || mapped.getColumns().isEmpty()) { + return namingStrategy.resolveColumnName(getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; + } + + ColumnConfig primaryCol = mapped.getColumns().get(0); + Object rawIndex = primaryCol.getIndex(); + if (rawIndex instanceof groovy.lang.Closure) { + PropertyConfig indexColConfig = PropertyConfig.configureNew((groovy.lang.Closure<?>) rawIndex); + if (!indexColConfig.getColumns().isEmpty()) { + String name = indexColConfig.getColumns().get(0).getName(); + if (StringUtils.hasText(name)) { + return name; + } + } + } + + try { + Map<String, String> indexMap = primaryCol.getIndexAsMap(); + String colName = indexMap.get("column"); + + if (StringUtils.hasText(colName)) { + return colName; + } + } + catch (Exception e) { + // ignore + } + + return namingStrategy.resolveColumnName(getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; } - default String getIndexColumnName(PersistentEntityNamingStrategy namingStrategy) { - return java.util.Optional.ofNullable(getMappedForm()) - .map(PropertyConfig::getIndexColumn) - .map(PropertyConfig::getColumn) - .orElseGet(() -> namingStrategy.resolveColumnName(getName()) - + GrailsDomainBinder.UNDERSCORE - + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME); + default String getIndexColumnType(String defaultType) { + PropertyConfig mapped = getMappedForm(); + + if (mapped != null && mapped.getIndexColumn() != null) { + PropertyConfig indexColConfig = mapped.getIndexColumn(); + if (StringUtils.hasText(indexColConfig.getTypeName())) { + return indexColConfig.getTypeName(); + } + } + + if (mapped == null || mapped.getColumns().isEmpty()) { + return defaultType; + } + + ColumnConfig primaryCol = mapped.getColumns().get(0); + Object rawIndex = primaryCol.getIndex(); + if (rawIndex instanceof groovy.lang.Closure) { + PropertyConfig indexColConfig = PropertyConfig.configureNew((groovy.lang.Closure<?>) rawIndex); + if (StringUtils.hasText(indexColConfig.getTypeName())) { + return indexColConfig.getTypeName(); + } + } + + try { + Map<String, String> indexMap = primaryCol.getIndexAsMap(); + String typeName = indexMap.get("type"); + + if (StringUtils.hasText(typeName)) { + return typeName; + } + } + catch (Exception e) { + // ignore + } + + return defaultType; } default String getMapElementName(PersistentEntityNamingStrategy namingStrategy) { @@ -128,9 +194,9 @@ public interface HibernateToManyProperty extends PropertyWithMapping<PropertyCon .map(PropertyConfig::getJoinTableColumnConfig) .map(ColumnConfig::getName) .orElseGet(() -> namingStrategy.resolveColumnName(getHibernateAssociatedEntity() - .getHibernateRootEntity() - .getJavaClass() - .getSimpleName()) + .getHibernateRootEntity() + .getJavaClass() + .getSimpleName()) + GrailsDomainBinder.FOREIGN_KEY_SUFFIX); } @@ -162,4 +228,4 @@ public interface HibernateToManyProperty extends PropertyWithMapping<PropertyCon void setCollection(Collection collection); Collection getCollection(); -} +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java index 315c28ada7..fe8d68ae51 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java @@ -20,21 +20,23 @@ package org.grails.orm.hibernate.cfg.domainbinding.secondpass; import java.util.*; import java.util.Map; - import jakarta.annotation.Nonnull; +import org.grails.datastore.mapping.model.types.Basic; import org.hibernate.MappingException; import org.hibernate.mapping.*; import org.hibernate.mapping.Collection; - import org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionForPropertyConfigBinder; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToManyProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty; -/** Refactored from CollectionBinder to handle collection second pass binding. */ +/** + * Refactored from CollectionBinder to handle collection second pass binding. + */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CollectionSecondPassBinder { + private final CollectionOrderByBinder collectionOrderByBinder; private final CollectionMultiTenantFilterBinder collectionMultiTenantFilterBinder; private final CollectionKeyColumnUpdater collectionKeyColumnUpdater; @@ -44,7 +46,6 @@ public class CollectionSecondPassBinder { private final CollectionWithJoinTableBinder collectionWithJoinTableBinder; private final CollectionForPropertyConfigBinder collectionForPropertyConfigBinder; - /** Creates a new {@link CollectionSecondPassBinder} instance. */ public CollectionSecondPassBinder( CollectionKeyColumnUpdater collectionKeyColumnUpdater, UnidirectionalOneToManyBinder unidirectionalOneToManyBinder, @@ -64,27 +65,29 @@ public class CollectionSecondPassBinder { this.collectionMultiTenantFilterBinder = collectionMultiTenantFilterBinder; } - /** Bind collection second pass. */ - public void bindCollectionSecondPass( - @Nonnull HibernateToManyProperty property, - Map<?, ?> persistentClasses) { - + public void bindCollectionSecondPass(@Nonnull HibernateToManyProperty property, Map<?, ?> persistentClasses) { Collection collection = property.getCollection(); - PersistentClass associatedClass = resolveAssociatedClass(property, persistentClasses); - collectionOrderByBinder.bind(property, associatedClass); - bindOneToManyAssociation(property, associatedClass); - - collectionMultiTenantFilterBinder.bind(property); - collection.setSorted(property.isSorted()); - collectionKeyColumnUpdater.bind(property, associatedClass); - collection.setCacheConcurrencyStrategy(property.getCacheUsage()); - - bindCollectionElement(property); + if (property instanceof Basic) { + // Basic collections (scalars/enums) don't have an associated PersistentClass + collectionMultiTenantFilterBinder.bind(property); + collection.setSorted(property.isSorted()); + collectionKeyColumnUpdater.bind(property, null); + collection.setCacheConcurrencyStrategy(property.getCacheUsage()); + bindCollectionElement(property); + } else { + PersistentClass associatedClass = resolveAssociatedClass(property, persistentClasses); + collectionOrderByBinder.bind(property, associatedClass); + bindOneToManyAssociation(property, associatedClass); + collectionMultiTenantFilterBinder.bind(property); + collection.setSorted(property.isSorted()); + collectionKeyColumnUpdater.bind(property, associatedClass); + collection.setCacheConcurrencyStrategy(property.getCacheUsage()); + bindCollectionElement(property); + } } - private void bindOneToManyAssociation( - HibernateToManyProperty property, PersistentClass associatedClass) { + private void bindOneToManyAssociation(HibernateToManyProperty property, PersistentClass associatedClass) { Collection collection = property.getCollection(); if (!collection.isOneToMany()) { return; @@ -97,25 +100,21 @@ public class CollectionSecondPassBinder { collectionForPropertyConfigBinder.bindCollectionForPropertyConfig(property); } - private void bindCollectionElement( - HibernateToManyProperty property) { + private void bindCollectionElement(HibernateToManyProperty property) { if (property instanceof HibernateManyToManyProperty manyToMany && manyToMany.isBidirectional()) { manyToManyElementBinder.bind(manyToMany); } else if (property.isBidirectionalOneToManyMap() && property.isBidirectional()) { bidirectionalMapElementBinder.bind(property); - } else if (property instanceof HibernateOneToManyProperty oneToManyProperty - && oneToManyProperty.isUnidirectionalOneToMany()) { + } else if (property instanceof HibernateOneToManyProperty oneToManyProperty && oneToManyProperty.isUnidirectionalOneToMany()) { unidirectionalOneToManyBinder.bind(oneToManyProperty); } else if (property.supportsJoinColumnMapping()) { collectionWithJoinTableBinder.bindCollectionWithJoinTable(property); } } - private @Nonnull PersistentClass resolveAssociatedClass( - HibernateToManyProperty property, Map<?, ?> persistentClasses) { + protected PersistentClass resolveAssociatedClass(HibernateToManyProperty property, Map<?, ?> persistentClasses) { return Optional.ofNullable(property.getHibernateAssociatedEntity()) .map(referenced -> (PersistentClass) persistentClasses.get(referenced.getName())) - .orElseThrow( - () -> new MappingException("Association [" + property.getName() + "] has no associated class")); + .orElseThrow(() -> new MappingException("Association [" + property.getName() + "] has no associated class")); } -} +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java index c9be0c3066..41a972206d 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java @@ -1,20 +1,20 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package org.grails.orm.hibernate.cfg.domainbinding.util; @@ -37,28 +37,38 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentP import static org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehavior.*; -/** The cascade behavior fetcher class. */ +/** + * The cascade behavior fetcher class. + */ public class CascadeBehaviorFetcher { private static final Logger LOG = LoggerFactory.getLogger(CascadeBehaviorFetcher.class); private final LogCascadeMapping logCascadeMapping; - /** Creates a new {@link CascadeBehaviorFetcher} instance. */ + /** + * Creates a new {@link CascadeBehaviorFetcher} instance. + */ public CascadeBehaviorFetcher(LogCascadeMapping logCascadeMapping) { this.logCascadeMapping = logCascadeMapping; } - /** Creates a new {@link CascadeBehaviorFetcher} instance. */ + /** + * Creates a new {@link CascadeBehaviorFetcher} instance. + */ public CascadeBehaviorFetcher() { this(new LogCascadeMapping(LOG)); } - /** Gets the cascade behaviour. */ + /** + * Gets the cascade behaviour. + */ public String getCascadeBehaviour(Association<?> association) { - var cascadeStrategy = - getDefinedBehavior((HibernatePersistentProperty) association).orElse(getImpliedBehavior(association)); + var cascadeStrategy = getDefinedBehavior((HibernatePersistentProperty) association) + .orElse(getImpliedBehavior(association)); + logCascadeMapping.logCascadeMapping(association, cascadeStrategy); + return cascadeStrategy.getValue(); } @@ -69,35 +79,50 @@ public class CascadeBehaviorFetcher { } private CascadeBehavior getImpliedBehavior(Association<?> association) { - if (association.getAssociatedEntity() == null) { - // NEW BEHAVIOR, FAIL-FAST - throw new MappingException("Relationship " + association + " has no associated entity"); + // Handle types that do not require an associated entity first + if (association instanceof Basic) { + return ALL; } + + if (Map.class.isAssignableFrom(association.getType())) { + return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE; + } + if (association instanceof Embedded) { return ALL; } + + // Fail-fast only for entity relationships that are truly missing an association + if (association.getAssociatedEntity() == null) { + throw new MappingException("Relationship " + association + " has no associated entity"); + } + if (association.isHasOne()) { return ALL; - } else if (association instanceof HibernateOneToOneProperty) { + } + else if (association instanceof HibernateOneToOneProperty) { return association.isOwningSide() ? ALL : SAVE_UPDATE; - } else if (association instanceof HibernateOneToManyProperty) { + } + else if (association instanceof HibernateOneToManyProperty) { return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE; - } else if (association instanceof HibernateManyToManyProperty) { + } + else if (association instanceof HibernateManyToManyProperty) { return association.isCorrectlyOwned() || association.isCircular() ? SAVE_UPDATE : NONE; - } else if (association instanceof HibernateManyToOneProperty) { + } + else if (association instanceof HibernateManyToOneProperty) { if (association.isCorrectlyOwned() && !association.isCircular()) { return ALL; - } else if (association.isCompositeIdProperty()) { + } + else if (association.isCompositeIdProperty()) { return ALL; - } else { + } + else { return NONE; } - } else if (association instanceof Basic) { - return ALL; - } else if (Map.class.isAssignableFrom(association.getType())) { - return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE; - } else { + } + else { throw new MappingException("Unrecognized association type " + association.getType()); } } -} + +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy index d9bd858484..e9f8d6f75b 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy @@ -40,6 +40,462 @@ class HibernateMappingBuilderSpec extends Specification { builder().evaluate(cl) } + // ------------------------------------------------------------------------- + // table / catalog / schema / comment + // ------------------------------------------------------------------------- + + def "table with name only"() { + when: + Mapping m = evaluate { table 'myTable' } + + then: + m.tableName == 'myTable' + } + + def "table with catalog and schema"() { + when: + Mapping m = evaluate { table name: 'table', catalog: 'CRM', schema: 'dbo' } + + then: + m.table.name == 'table' + m.table.schema == 'dbo' + m.table.catalog == 'CRM' + } + + def "table comment is stored"() { + when: + Mapping m = evaluate { comment 'wahoo' } + + then: + m.comment == 'wahoo' + } + + // ------------------------------------------------------------------------- + // version / autoTimestamp + // ------------------------------------------------------------------------- + + def "version column can be changed"() { + when: + Mapping m = evaluate { version 'v_number' } + + then: + m.getPropertyConfig("version").column == 'v_number' + } + + def "versioning can be disabled"() { + when: + Mapping m = evaluate { version false } + + then: + !m.versioned + } + + def "autoTimestamp can be disabled"() { + when: + Mapping m = evaluate { autoTimestamp false } + + then: + !m.autoTimestamp + } + + // ------------------------------------------------------------------------- + // discriminator + // ------------------------------------------------------------------------- + + def "discriminator value only"() { + when: + Mapping m = evaluate { discriminator 'one' } + + then: + m.discriminator.value == 'one' + m.discriminator.column == null + } + + def "discriminator with column name"() { + when: + Mapping m = evaluate { discriminator value: 'one', column: 'type' } + + then: + m.discriminator.value == 'one' + m.discriminator.column.name == 'type' + } + + def "discriminator with column map"() { + when: + Mapping m = evaluate { discriminator value: 'one', column: [name: 'type', sqlType: 'integer'] } + + then: + m.discriminator.value == 'one' + m.discriminator.column.name == 'type' + m.discriminator.column.sqlType == 'integer' + } + + def "discriminator with formula and other settings"() { + when: + Mapping m = evaluate { + discriminator value: '1', formula: "case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end", type: 'integer', insert: false + } + + then: + m.discriminator.value == '1' + m.discriminator.formula == "case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end" + m.discriminator.type == 'integer' + !m.discriminator.insertable + } + + // ------------------------------------------------------------------------- + // inheritance + // ------------------------------------------------------------------------- + + def "tablePerHierarchy false disables it"() { + when: + Mapping m = evaluate { tablePerHierarchy false } + + then: + !m.tablePerHierarchy + } + + def "tablePerSubclass true disables tablePerHierarchy"() { + when: + Mapping m = evaluate { tablePerSubclass true } + + then: + !m.tablePerHierarchy + } + + def "tablePerConcreteClass true enables it and disables tablePerHierarchy"() { + when: + Mapping m = evaluate { tablePerConcreteClass true } + + then: + m.tablePerConcreteClass + !m.tablePerHierarchy + } + + // ------------------------------------------------------------------------- + // cache settings + // ------------------------------------------------------------------------- + + def "default cache strategy"() { + when: + Mapping m = evaluate { cache true } + + then: + m.cache.usage.toString() == 'read-write' + m.cache.include.toString() == 'all' + } + + def "custom cache strategy"() { + when: + Mapping m = evaluate { cache usage: 'read-only', include: 'non-lazy' } + + then: + m.cache.usage.toString() == 'read-only' + m.cache.include.toString() == 'non-lazy' + } + + def "custom cache strategy with usage string only"() { + when: + Mapping m = evaluate { cache 'read-only' } + + then: + m.cache.usage.toString() == 'read-only' + m.cache.include.toString() == 'all' + } + + def "invalid cache values are ignored and defaults used"() { + when: + Mapping m = evaluate { cache usage: 'rubbish', include: 'more-rubbish' } + + then: + m.cache.usage.toString() == 'read-write' + m.cache.include.toString() == 'all' + } + + // ------------------------------------------------------------------------- + // identity / id + // ------------------------------------------------------------------------- + + def "identity column mapping"() { + when: + Mapping m = evaluate { id column: 'foo_id', type: Integer } + + then: + m.identity.type == Long // Default remains Long? No, wait. + // In HibernateMappingBuilderTests: + // assertEquals Long, mapping.identity.type + // assertEquals 'foo_id', mapping.getPropertyConfig("id").column + // assertEquals Integer, mapping.getPropertyConfig("id").type + m.getPropertyConfig("id").column == 'foo_id' + m.getPropertyConfig("id").type == Integer + m.identity.generator == 'native' + } + + def "default id strategy"() { + when: + Mapping m = evaluate { } + + then: + m.identity.type == Long + m.identity.column == 'id' + m.identity.generator == 'native' + } + + def "hilo id strategy"() { + when: + Mapping m = evaluate { id generator: 'hilo', params: [table: 'hi_value', column: 'next_value', max_lo: 100] } + + then: + m.identity.column == 'id' + m.identity.generator == 'hilo' + m.identity.params.table == 'hi_value' + } + + def "composite id strategy"() { + when: + Mapping m = evaluate { id composite: ['one', 'two'], compositeClass: HibernateMappingBuilder } + + then: + m.identity instanceof org.grails.orm.hibernate.cfg.CompositeIdentity + m.identity.propertyNames == ['one', 'two'] + m.identity.compositeClass == HibernateMappingBuilder + } + + def "natural id mapping"() { + expect: + evaluate { id natural: 'one' }.identity.natural.propertyNames == ['one'] + evaluate { id natural: ['one', 'two'] }.identity.natural.propertyNames == ['one', 'two'] + evaluate { id natural: [properties: ['one', 'two'], mutable: true] }.identity.natural.mutable + } + + // ------------------------------------------------------------------------- + // other root settings + // ------------------------------------------------------------------------- + + def "autoImport defaults to true and can be disabled"() { + expect: + evaluate { }.autoImport + !evaluate { autoImport false }.autoImport + } + + def "dynamicUpdate and dynamicInsert"() { + when: + Mapping m = evaluate { + dynamicUpdate true + dynamicInsert true + } + + then: + m.dynamicUpdate + m.dynamicInsert + + when: + m = evaluate { } + + then: + !m.dynamicUpdate + !m.dynamicInsert + } + + def "batchSize config"() { + when: + Mapping m = evaluate { + batchSize 10 + things batchSize: 15 + } + + then: + m.batchSize == 10 + m.getPropertyConfig('things').batchSize == 15 + } + + def "class sort order"() { + when: + Mapping m = evaluate { + sort "name" + order "desc" + } + + then: + m.sort.name == "name" + m.sort.direction == "desc" + } + + def "class sort order via map"() { + when: + Mapping m = evaluate { + sort name: 'desc' + } + + then: + m.sort.namesAndDirections == [name: 'desc'] + } + + def "property ignoreNotFound is stored"() { + expect: + evaluate { foos ignoreNotFound: true }.getPropertyConfig("foos").ignoreNotFound + !evaluate { foos ignoreNotFound: false }.getPropertyConfig("foos").ignoreNotFound + } + + def "property association sort order"() { + when: + Mapping m = evaluate { + columns { + things sort: 'name' + } + } + + then: + m.getPropertyConfig('things').sort == 'name' + } + + def "property lazy settings"() { + expect: + evaluate { things column: 'foo' }.getPropertyConfig('things').getLazy() == null + !evaluate { things lazy: false }.getPropertyConfig('things').lazy + } + + def "property cascades"() { + expect: + evaluate { things cascade: 'persist,merge' }.getPropertyConfig('things').cascade == 'persist,merge' + evaluate { columns { things cascade: 'all' } }.getPropertyConfig('things').cascade == 'all' + } + + def "property fetch modes"() { + expect: + evaluate { things fetch: 'join' }.getPropertyConfig('things').fetchMode == FetchMode.JOIN + evaluate { things fetch: 'select' }.getPropertyConfig('things').fetchMode == FetchMode.SELECT + evaluate { things column: 'foo' }.getPropertyConfig('things').fetchMode == FetchMode.DEFAULT + } + + def "property enumType"() { + expect: + evaluate { things column: 'foo' }.getPropertyConfig('things').enumType == 'default' + evaluate { things enumType: 'ordinal' }.getPropertyConfig('things').enumType == 'ordinal' + } + + def "property joinTable mapping"() { + when: + Mapping m1 = evaluate { things joinTable: true } + Mapping m2 = evaluate { things joinTable: 'foo' } + Mapping m3 = evaluate { things joinTable: [name: 'foo', key: 'foo_id', column: 'bar_id'] } + + then: + m1.getPropertyConfig('things').joinTable != null + m2.getPropertyConfig('things').joinTable.name == 'foo' + m3.getPropertyConfig('things').joinTable.name == 'foo' + m3.getPropertyConfig('things').joinTable.key.name == 'foo_id' + m3.getPropertyConfig('things').joinTable.column.name == 'bar_id' + } + + def "property custom association caching"() { + when: + Mapping m1 = evaluate { firstName cache: [usage: 'read-only', include: 'non-lazy'] } + Mapping m2 = evaluate { firstName cache: 'read-only' } + Mapping m3 = evaluate { firstName cache: true } + + then: + m1.getPropertyConfig('firstName').cache.usage.toString() == 'read-only' + m1.getPropertyConfig('firstName').cache.include.toString() == 'non-lazy' + m2.getPropertyConfig('firstName').cache.usage.toString() == 'read-only' + m3.getPropertyConfig('firstName').cache.usage.toString() == 'read-write' + m3.getPropertyConfig('firstName').cache.include.toString() == 'all' + } + + def "simple column mappings"() { + when: + Mapping m = evaluate { + firstName column: 'First_Name' + lastName column: 'Last_Name' + } + + then: + m.getPropertyConfig('firstName').column == 'First_Name' + m.getPropertyConfig('lastName').column == 'Last_Name' + } + + def "complex column mappings"() { + when: + Mapping m = evaluate { + firstName column: 'First_Name', + lazy: true, + unique: true, + type: java.sql.Clob, + length: 255, + index: 'foo', + sqlType: 'text' + } + + then: + m.columns.firstName.column == 'First_Name' + m.columns.firstName.lazy + m.columns.firstName.isUnique() + m.columns.firstName.type == java.sql.Clob + m.columns.firstName.length == 255 + m.columns.firstName.getIndexName() == 'foo' + m.columns.firstName.sqlType == 'text' + } + + def "property with multiple columns"() { + when: + Mapping m = evaluate { + amount type: MyUserType, { + column name: "value" + column name: "currency", sqlType: "char", length: 3 + } + } + + then: + m.columns.amount.columns.size() == 2 + m.columns.amount.columns[0].name == "value" + m.columns.amount.columns[1].name == "currency" + m.columns.amount.columns[1].sqlType == "char" + m.columns.amount.columns[1].length == 3 + } + + def "disallowed multi-column property access"() { + given: + def b = builder() + b.evaluate { + amount type: MyUserType, { + column name: "value" + column name: "currency" + } + } + + when: + b.evaluate { amount scale: 2 } + + then: + thrown(Throwable) + } + + def "property with user type and params"() { + when: + Mapping m = evaluate { + amount type: MyUserType, params: [param1: "amountParam1", param2: 65] + } + + then: + m.getPropertyConfig('amount').type == MyUserType + m.getPropertyConfig('amount').typeParams.param1 == "amountParam1" + m.getPropertyConfig('amount').typeParams.param2 == 65 + } + + def "property insertable and updatable"() { + when: + Mapping m = evaluate { + firstName insertable: true, updatable: true + lastName insertable: false, updatable: false + } + + then: + m.getPropertyConfig('firstName').insertable + m.getPropertyConfig('firstName').updatable + !m.getPropertyConfig('lastName').insertable + !m.getPropertyConfig('lastName').updatable + } + // ------------------------------------------------------------------------- // autowire / tenantId // ------------------------------------------------------------------------- @@ -399,4 +855,6 @@ class HibernateMappingBuilderSpec extends Specification { noExceptionThrown() m.getPropertyConfig('myProp') == null } + + static class MyUserType {} } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy deleted file mode 100644 index ac586e0477..0000000000 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy +++ /dev/null @@ -1,902 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package grails.gorm.hibernate.mapping - -import java.sql.Clob - -import org.grails.orm.hibernate.cfg.CompositeIdentity -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateMappingBuilder -import org.grails.orm.hibernate.cfg.PropertyConfig - -import org.hibernate.FetchMode -import org.junit.jupiter.api.Test - -import static org.junit.jupiter.api.Assertions.assertEquals -import static org.junit.jupiter.api.Assertions.assertFalse -import static org.junit.jupiter.api.Assertions.assertNull -import static org.junit.jupiter.api.Assertions.assertThrows -import static org.junit.jupiter.api.Assertions.assertTrue - -/** - * Tests that the Hibernate mapping DSL constructs a valid Mapping object. - * - * @author Graeme Rocher - * @since 1.0 - */ -class HibernateMappingBuilderTests { - -// void testWildcardApplyToAllProperties() { -// def builder = new HibernateMappingBuilder("Foo") -// def mapping = builder.evaluate { -// '*'(column:"foo") -// '*-1'(column:"foo") -// '1-1'(column:"foo") -// '1-*'(column:"foo") -// '*-*'(column:"foo") -// one cache:true -// two ignoreNoteFound:false -// } -// } - - @Test - void testIncludes() { - def callable = { - foos lazy:false - } - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - includes callable - foos ignoreNotFound:true - } - - def pc = mapping.getPropertyConfig("foos") - assert pc.ignoreNotFound : "should have ignoreNotFound enabled" - assert !pc.lazy : "should not be lazy" - } - - @Test - void testIgnoreNotFound() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - foos ignoreNotFound:true - } - - assertTrue mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been true" - - mapping = builder.evaluate { - foos ignoreNotFound:false - } - assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been false" - - mapping = builder.evaluate { // default - foos lazy:false - } - assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been false" - } - - @Test - void testNaturalId() { - - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - id natural: 'one' - } - - assertEquals(['one'], mapping.identity.natural.propertyNames) - - mapping = builder.evaluate { - id natural: ['one','two'] - } - - assertEquals(['one','two'], mapping.identity.natural.propertyNames) - - mapping = builder.evaluate { - id natural: [properties:['one','two'], mutable:true] - } - - assertEquals(['one','two'], mapping.identity.natural.propertyNames) - assertTrue mapping.identity.natural.mutable - } - - @Test - void testDiscriminator() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - discriminator 'one' - } - - assertEquals "one", mapping.discriminator.value - assertNull mapping.discriminator.column - - mapping = builder.evaluate { - discriminator value:'one', column:'type' - } - - assertEquals "one", mapping.discriminator.value - assertEquals "type", mapping.discriminator.column.name - - mapping = builder.evaluate { - discriminator value:'one', column:[name:'type', sqlType:'integer'] - } - - assertEquals "one", mapping.discriminator.value - assertEquals "type", mapping.discriminator.column.name - assertEquals "integer", mapping.discriminator.column.sqlType - } - - @Test - void testDiscriminatorMap() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - discriminator value:'1', formula:"case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end",type:'integer',insert:false - } - - assertEquals "1", mapping.discriminator.value - assertNull mapping.discriminator.column - - assertEquals "case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end", mapping.discriminator.formula - assertEquals "integer", mapping.discriminator.type - assertFalse mapping.discriminator.insertable - } - - @Test - void testAutoImport() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { } - - assertTrue mapping.autoImport, "default auto-import should be true" - - mapping = builder.evaluate { - autoImport false - } - - assertFalse mapping.autoImport, "auto-import should be false" - } - - @Test - void testTableWithCatalogueAndSchema() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table name:"table", catalog:"CRM", schema:"dbo" - } - - assertEquals 'table',mapping.table.name - assertEquals 'dbo',mapping.table.schema - assertEquals 'CRM',mapping.table.catalog - } - - @Test - void testIndexColumn() { - - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - things indexColumn:[name:"chapter_number", type:"string", length:3] - } - - PropertyConfig pc = mapping.getPropertyConfig("things") - assertEquals "chapter_number",pc.indexColumn.column - assertEquals "string",pc.indexColumn.type - assertEquals 3, pc.indexColumn.length - } - - @Test - void testDynamicUpdate() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - dynamicUpdate true - dynamicInsert true - } - - assertTrue mapping.dynamicUpdate - assertTrue mapping.dynamicInsert - - builder = new HibernateMappingBuilder("Foo") - mapping = builder.evaluate {} - - assertFalse mapping.dynamicUpdate - assertFalse mapping.dynamicInsert - } - - @Test - void testBatchSizeConfig() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - batchSize 10 - things batchSize:15 - } - - assertEquals 10, mapping.batchSize - assertEquals 15,mapping.getPropertyConfig('things').batchSize - } - - @Test - void testChangeVersionColumn() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - version 'v_number' - } - - assertEquals 'v_number', mapping.getPropertyConfig("version").column - } - - @Test - void testClassSortOrder() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - sort "name" - order "desc" - columns { - things sort:'name' - } - } - - assertEquals "name", mapping.sort.name - assertEquals "desc", mapping.sort.direction - assertEquals 'name',mapping.getPropertyConfig('things').sort - - mapping = builder.evaluate { - sort name:'desc' - - columns { - things sort:'name' - } - } - - assertEquals "name", mapping.sort.name - assertEquals "desc", mapping.sort.direction - assertEquals 'name',mapping.getPropertyConfig('things').sort - } - - @Test - void testAssociationSortOrder() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things sort:'name' - } - } - - assertEquals 'name',mapping.getPropertyConfig('things').sort - } - - @Test - void testLazy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things cascade:'persist,merge' - } - } - - assertNull mapping.getPropertyConfig('things').getLazy(), "should have been lazy" - - mapping = builder.evaluate { - columns { - things lazy:false - } - } - - assertFalse mapping.getPropertyConfig('things').lazy, "shouldn't have been lazy" - } - - @Test - void testCascades() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things cascade:'persist,merge' - } - } - - assertEquals 'persist,merge',mapping.getPropertyConfig('things').cascade - } - - @Test - void testFetchModes() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things fetch:'join' - others fetch:'select' - mores column:'yuck' - } - } - - assertEquals FetchMode.JOIN,mapping.getPropertyConfig('things').fetchMode - assertEquals FetchMode.SELECT,mapping.getPropertyConfig('others').fetchMode - assertEquals FetchMode.DEFAULT,mapping.getPropertyConfig('mores').fetchMode - } - - @Test - void testEnumType() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things column:'foo' - } - } - - assertEquals 'default',mapping.getPropertyConfig('things').enumType - - mapping = builder.evaluate { - columns { - things enumType:'ordinal' - } - } - - assertEquals 'ordinal',mapping.getPropertyConfig('things').enumType - } - - @Test - void testCascadesWithColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - things cascade:'persist,merge' - } - assertEquals 'persist,merge',mapping.getPropertyConfig('things').cascade - } - - @Test - void testJoinTableMapping() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - columns { - things joinTable:true - } - } - - assert mapping.getPropertyConfig('things')?.joinTable - - mapping = builder.evaluate { - columns { - things joinTable:'foo' - } - } - - PropertyConfig property = mapping.getPropertyConfig('things') - assert property?.joinTable - assertEquals "foo", property.joinTable.name - - mapping = builder.evaluate { - columns { - things joinTable:[name:'foo', key:'foo_id', column:'bar_id'] - } - } - - property = mapping.getPropertyConfig('things') - assert property?.joinTable - assertEquals "foo", property.joinTable.name - assertEquals "foo_id", property.joinTable.key.name - assertEquals "bar_id", property.joinTable.column.name - } - - @Test - void testJoinTableMappingWithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - things joinTable:true - } - - assert mapping.getPropertyConfig('things')?.joinTable - - mapping = builder.evaluate { - things joinTable:'foo' - } - - PropertyConfig property = mapping.getPropertyConfig('things') - assert property?.joinTable - assertEquals "foo", property.joinTable.name - - mapping = builder.evaluate { - things joinTable:[name:'foo', key:'foo_id', column:'bar_id'] - } - - property = mapping.getPropertyConfig('things') - assert property?.joinTable - assertEquals "foo", property.joinTable.name - assertEquals "foo_id", property.joinTable.key.name - assertEquals "bar_id", property.joinTable.column.name - } - - @Test - void testCustomInheritanceStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - tablePerHierarchy false - } - - assertFalse mapping.tablePerHierarchy - - mapping = builder.evaluate { - table 'myTable' - tablePerSubclass true - } - - assertFalse mapping.tablePerHierarchy - } - - @Test - void testTablePerConcreteClass() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - tablePerConcreteClass true - } - - assertTrue mapping.tablePerConcreteClass - assertFalse mapping.tablePerHierarchy - } - - @Test - void testAutoTimeStamp() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - autoTimestamp false - } - - assertFalse mapping.autoTimestamp - } - - @Test - void testCustomAssociationCachingConfig1() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - columns { - firstName cache:[usage:'read-only', include:'non-lazy'] - } - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-only', cc.cache.usage.toString() - assertEquals 'non-lazy', cc.cache.include.toString() - } - - @Test - void testCustomAssociationCachingConfig1WithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - firstName cache:[usage:'read-only', include:'non-lazy'] - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-only', cc.cache.usage.toString() - assertEquals 'non-lazy', cc.cache.include.toString() - } - - @Test - void testCustomAssociationCachingConfig2() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - - columns { - firstName cache:'read-only' - } - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-only', cc.cache.usage.toString() - } - - @Test - void testCustomAssociationCachingConfig2WithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - firstName cache:'read-only' - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-only', cc.cache.usage.toString() - } - - @Test - void testAssociationCachingConfig() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - - columns { - firstName cache:true - } - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-write', cc.cache.usage.toString() - assertEquals 'all', cc.cache.include.toString() - } - - @Test - void testAssociationCachingConfigWithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - firstName cache:true - } - - def cc = mapping.getPropertyConfig('firstName') - assertEquals 'read-write', cc.cache.usage.toString() - assertEquals 'all', cc.cache.include.toString() - } - - @Test - void testEvaluateTableName() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - } - - assertEquals 'myTable', mapping.tableName - } - - @Test - void testDefaultCacheStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - cache true - } - - assertEquals 'read-write', mapping.cache.usage.toString() - assertEquals 'all', mapping.cache.include.toString() - } - - @Test - void testCustomCacheStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - cache usage:'read-only', include:'non-lazy' - } - - assertEquals 'read-only', mapping.cache.usage.toString() - assertEquals 'non-lazy', mapping.cache.include.toString() - } - - @Test - void testCustomCacheStrategy2() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - cache 'read-only' - } - - assertEquals 'read-only', mapping.cache.usage.toString() - assertEquals 'all', mapping.cache.include.toString() - } - - @Test - void testInvalidCacheValues() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - cache usage:'rubbish', include:'more-rubbish' - } - - // should be ignored and logged to console - assertEquals 'read-write', mapping.cache.usage.toString() - assertEquals 'all', mapping.cache.include.toString() - } - - @Test - void testEvaluateVersioning() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - } - - assertEquals 'myTable', mapping.tableName - assertFalse mapping.versioned - } - - @Test - void testIdentityColumnMapping() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - id column:'foo_id', type:Integer - } - - assertEquals Long, mapping.identity.type - assertEquals 'foo_id', mapping.getPropertyConfig("id").column - assertEquals Integer, mapping.getPropertyConfig("id").type - assertEquals 'native', mapping.identity.generator - } - - @Test - void testDefaultIdStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - } - - assertEquals Long, mapping.identity.type - assertEquals 'id', mapping.identity.column - assertEquals 'native', mapping.identity.generator - } - - @Test - void testHiloIdStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100] - } - - assertEquals Long, mapping.identity.type - assertEquals 'id', mapping.identity.column - assertEquals 'hilo', mapping.identity.generator - assertEquals 'hi_value', mapping.identity.params.table - } - - @Test - void testCompositeIdStrategy() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - id composite:['one','two'], compositeClass:HibernateMappingBuilder - } - def compositeId = mapping.identity - - assert compositeId instanceof CompositeIdentity - - assertEquals( "one", compositeId.propertyNames[0]) - assertEquals "two", compositeId.propertyNames[1] - assertEquals HibernateMappingBuilder, compositeId.compositeClass - } - - @Test - void testSimpleColumnMappingsWithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - firstName column:'First_Name' - lastName column:'Last_Name' - } - - assertEquals "First_Name",mapping.getPropertyConfig('firstName').column - assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column - } - - @Test - void testSimpleColumnMappings() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - columns { - firstName column:'First_Name' - lastName column:'Last_Name' - } - } - - assertEquals "First_Name",mapping.getPropertyConfig('firstName').column - assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column - } - - @Test - void testComplexColumnMappings() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - columns { - firstName column:'First_Name', - lazy:true, - unique:true, - type: Clob, - length:255, - index:'foo', - sqlType: 'text' - - lastName column:'Last_Name' - } - } - - assertEquals "First_Name",mapping.columns.firstName.column - assertTrue mapping.columns.firstName.lazy - assertTrue mapping.columns.firstName.unique - assertEquals Clob,mapping.columns.firstName.type - assertEquals 255,mapping.columns.firstName.length - assertEquals 'foo',mapping.columns.firstName.getIndexName() - assertEquals "text",mapping.columns.firstName.sqlType - assertEquals "Last_Name",mapping.columns.lastName.column - } - - @Test - void testComplexColumnMappingsWithoutColumnsBlock() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - table 'myTable' - version false - firstName column:'First_Name', - lazy:true, - unique:true, - type: Clob, - length:255, - index:'foo', - sqlType: 'text' - - lastName column:'Last_Name' - } - - assertEquals "First_Name",mapping.columns.firstName.column - assertTrue mapping.columns.firstName.lazy - assertTrue mapping.columns.firstName.unique - assertEquals Clob,mapping.columns.firstName.type - assertEquals 255,mapping.columns.firstName.length - assertEquals 'foo',mapping.columns.firstName.getIndexName() - assertEquals "text",mapping.columns.firstName.sqlType - assertEquals "Last_Name",mapping.columns.lastName.column - } - - @Test - void testPropertyWithMultipleColumns() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - amount type: MyUserType, { - column name: "value" - column name: "currency", sqlType: "char", length: 3 - } - } - - assertEquals 2, mapping.columns.amount.columns.size() - assertEquals "value", mapping.columns.amount.columns[0].name - assertEquals "currency", mapping.columns.amount.columns[1].name - assertEquals "char", mapping.columns.amount.columns[1].sqlType - assertEquals 3, mapping.columns.amount.columns[1].length - - assertThrows Throwable, { mapping.columns.amount.column } - assertThrows Throwable, { mapping.columns.amount.sqlType } - } - - @Test - void testConstrainedPropertyWithMultipleColumns() { - def builder = new HibernateMappingBuilder("Foo") - builder.evaluate { - amount type: MyUserType, { - column name: "value" - column name: "currency", sqlType: "char", length: 3 - } - } - def mapping = builder.evaluate { - amount nullable: true - } - - assertEquals 2, mapping.columns.amount.columns.size() - assertEquals "value", mapping.columns.amount.columns[0].name - assertEquals "currency", mapping.columns.amount.columns[1].name - assertEquals "char", mapping.columns.amount.columns[1].sqlType - assertEquals 3, mapping.columns.amount.columns[1].length - - assertThrows Throwable, { mapping.columns.amount.column } - assertThrows Throwable, { mapping.columns.amount.sqlType } - } - - @Test - void testDisallowedConstrainedPropertyWithMultipleColumns() { - def builder = new HibernateMappingBuilder("Foo") - builder.evaluate { - amount type: MyUserType, { - column name: "value" - column name: "currency", sqlType: "char", length: 3 - } - } - assertThrows(Throwable, { - builder.evaluate { - amount scale: 2 - } - }, "Cannot treat multi-column property as a single-column property") - } - - @Test - void testPropertyWithUserTypeAndNoParams() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - amount type: MyUserType - } - - assertEquals MyUserType, mapping.getPropertyConfig('amount').type - assertNull mapping.getPropertyConfig('amount').typeParams - } - - @Test - void testPropertyWithUserTypeAndTypeParams() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - amount type: MyUserType, params : [ param1 : "amountParam1", param2 : 65 ] - value type: MyUserType, params : [ param1 : "valueParam1", param2 : 21 ] - } - - assertEquals MyUserType, mapping.getPropertyConfig('amount').type - assertEquals "amountParam1", mapping.getPropertyConfig('amount').typeParams.param1 - assertEquals 65, mapping.getPropertyConfig('amount').typeParams.param2 - assertEquals MyUserType, mapping.getPropertyConfig('value').type - assertEquals "valueParam1", mapping.getPropertyConfig('value').typeParams.param1 - assertEquals 21, mapping.getPropertyConfig('value').typeParams.param2 - } - - @Test - void testInsertablePropertyConfig() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - firstName insertable:true - lastName insertable:false - } - assertTrue mapping.getPropertyConfig('firstName').insertable - assertFalse mapping.getPropertyConfig('lastName').insertable - } - - @Test - void testUpdatablePropertyConfig() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - firstName updatable:true - lastName updatable:false - } - assertTrue mapping.getPropertyConfig('firstName').updatable - assertFalse mapping.getPropertyConfig('lastName').updatable - } - - @Test - void testDefaultValue() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - comment 'wahoo' - name comment: 'bar' - foo defaultValue: '5' - } - assertEquals '5', mapping.getPropertyConfig('foo').columns[0].defaultValue - assertNull mapping.getPropertyConfig('name').columns[0].defaultValue - } - - @Test - void testColumnComment() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - comment 'wahoo' - name comment: 'bar' - foo defaultValue: '5' - } - assertEquals 'bar', mapping.getPropertyConfig('name').columns[0].comment - assertNull mapping.getPropertyConfig('foo').columns[0].comment - } - - @Test - void testTableComment() { - def builder = new HibernateMappingBuilder("Foo") - def mapping = builder.evaluate { - comment 'wahoo' - name comment: 'bar' - foo defaultValue: '5' - } - assertEquals 'wahoo', mapping.comment - } - // dummy user type - static class MyUserType {} -} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy index 40d12e9519..95e239aefd 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy @@ -101,7 +101,7 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T GrailsHibernatePersistentEntity createPersistentEntity(Class clazz, GrailsDomainBinder binder) { def entity = getMappingContext().addPersistentEntity(clazz) as GrailsHibernatePersistentEntity if (entity != null) { - MappingCacheHolder.getInstance().cacheMapping(entity) + getMappingContext().getMappingCacheHolder().cacheMapping(entity) } entity } @@ -166,6 +166,17 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T return new HibernateQuery(session, getPersistentEntity(clazz)) } + /** + * Triggers the first-pass Hibernate mapping for all registered entities. + * This initializes the Hibernate Collection, Table, and Column objects + * required for SecondPass binder tests. + */ + protected void hibernateFirstPass() { + def gdb = getGrailsDomainBinder() + def collector = gdb.getMetadataBuildingContext().getMetadataCollector() + gdb.contribute(collector, getMappingContext()) + } + /** * Returns true when a Docker daemon is reachable on this machine. * <p> diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy new file mode 100644 index 0000000000..0a543998f2 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import spock.lang.Specification +import spock.lang.Unroll + +class ColumnConfigSpec extends Specification { + + void "test default values"() { + when: + def config = new ColumnConfig() + + then: + config.enumType == 'default' + config.unique == false + config.length == -1 + config.precision == -1 + config.scale == -1 + } + + void "test configureNew with closure"() { + when: + def config = ColumnConfig.configureNew { + name "my_column" + sqlType "varchar(255)" + index "my_index" + unique true + length 100 + precision 10 + scale 2 + defaultValue "default_val" + comment "my comment" + read "read_sql" + write "write_sql" + } + + then: + config.name == "my_column" + config.sqlType == "varchar(255)" + config.index == "my_index" + config.unique == true + config.length == 100 + config.precision == 10 + config.scale == 2 + config.defaultValue == "default_val" + config.comment == "my comment" + config.read == "read_sql" + config.write == "write_sql" + } + + void "test configureNew with map"() { + when: + def config = ColumnConfig.configureNew( + name: "my_column", + sqlType: "varchar(255)", + index: "my_index", + unique: true, + length: 100, + precision: 10, + scale: 2, + defaultValue: "default_val", + comment: "my comment", + read: "read_sql", + write: "write_sql" + ) + + then: + config.name == "my_column" + config.sqlType == "varchar(255)" + config.index == "my_index" + config.unique == true + config.length == 100 + config.precision == 10 + config.scale == 2 + config.defaultValue == "default_val" + config.comment == "my comment" + config.read == "read_sql" + config.write == "write_sql" + } + + @Unroll + void "test getIndexAsMap with valid input: #input"() { + given: + def config = new ColumnConfig(index: input) + + expect: + config.getIndexAsMap() == expected + + where: + input | expected + null | [:] + [:] | [:] + [column: 'foo', type: 'string'] | [column: 'foo', type: 'string'] + "my_idx" | [column: "my_idx"] + "invalid_format" | [column: "invalid_format"] + "[]" | [:] + " " | [:] + "column:item_idx, type:integer" | [column: "item_idx", type: "integer"] + "[column:item_idx, type:integer]" | [column: "item_idx", type: "integer"] + "column:'item_idx', type:'integer'" | [column: "item_idx", type: "integer"] + 'column:"item_idx", type:"integer"' | [column: "item_idx", type: "integer"] + " column : item_idx , type : integer " | [column: "item_idx", type: "integer"] + } + + @Unroll + void "test getIndexAsMap with invalid input: #input"() { + given: + def config = new ColumnConfig(index: input) + + when: + config.getIndexAsMap() + + then: + thrown(IllegalArgumentException) + + where: + input << [ + "column:foo, invalid", + "column:foo, invalid:bar, extra" + ] + } + + void "test getIndexAsMap with non-string non-map input returns empty map"() { + given: + def config = new ColumnConfig(index: { "closure" }) + + expect: + config.getIndexAsMap() == [:] + } + + void "test toString"() { + given: + def config = new ColumnConfig(name: "foo", index: "bar", unique: true, length: 10, precision: 5, scale: 2) + + expect: + config.toString() == "column[name:foo, index:bar, unique:true, length:10, precision:5, scale:2]" + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy new file mode 100644 index 0000000000..4192a8f910 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import spock.lang.Specification + +class PropertyDefinitionDelegateSpec extends Specification { + + def "test column method with multiple columns"() { + given: + def config = new PropertyConfig() + def delegate = new PropertyDefinitionDelegate(config) + + when: + delegate.column(name: 'col1', sqlType: 'varchar(255)') + delegate.column(name: 'col2', sqlType: 'integer') + + then: + config.columns.size() == 2 + config.columns[0].name == 'col1' + config.columns[0].sqlType == 'varchar(255)' + config.columns[1].name == 'col2' + config.columns[1].sqlType == 'integer' + } + + def "test re-evaluation of column method with multiple columns"() { + given: + def config = new PropertyConfig() + def delegate1 = new PropertyDefinitionDelegate(config) + delegate1.column(name: 'col1', sqlType: 'varchar(255)') + delegate1.column(name: 'col2', sqlType: 'integer') + + when: "re-evaluating with a new delegate instance but same config" + def delegate2 = new PropertyDefinitionDelegate(config) + delegate2.column(name: 'new_col1', sqlType: 'text') + delegate2.column(name: 'new_col2', sqlType: 'long') + + then: + config.columns.size() == 2 + config.columns[0].name == 'new_col1' + config.columns[0].sqlType == 'text' + config.columns[1].name == 'new_col2' + config.columns[1].sqlType == 'long' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy index e322220694..abc8792b68 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy @@ -1,30 +1,27 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ - package org.grails.orm.hibernate.cfg.domainbinding import jakarta.persistence.Embeddable - import org.hibernate.MappingException import spock.lang.Shared import spock.lang.Unroll - import grails.gorm.annotation.Entity import grails.gorm.specs.HibernateGormDatastoreSpec import org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehaviorFetcher @@ -42,8 +39,6 @@ import static org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehavior.SA class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { - - // A single, comprehensive source of truth for all metadata test scenarios. private static final List cascadeMetadataTestData = [ // --- UNIDIRECTIONAL hasMany (should be supported in Hibernate 6+) --- @@ -57,28 +52,25 @@ class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { ["uni: explicit 'persist'", AW_Persist_Uni, "books", BookUni, PERSIST.getValue()], ["uni: invalid string", AW_Invalid_Uni, "books", BookUni, MappingException], - - - - // --- OTHER RELATIONSHIP TYPES --- - ["uni: string" , AW_Default_Uni , "books", BookUni , SAVE_UPDATE.getValue()], - ["uni: String" , AW_Default_String , "books", String , MappingException], - ["bi: default" , AW_Default_Bi , "books", Book_BT_Default , ALL.getValue()], - ["bi: hasOne (with belongsTo)" , AW_HasOne_Bi , "profile" , Profile_BT , ALL.getValue()], // Conservative default - ["uni: hasOne (no belongsTo)" , AW_HasOne_Uni , "passport", Passport , ALL.getValue()], // Should be supported - ["many-to-many (owning side)" , Post , "tags" , Tag_BT , SAVE_UPDATE.getValue()], + ["uni: string", AW_Default_Uni, "books", BookUni, SAVE_UPDATE.getValue()], + // FIX: This now expects ALL instead of MappingException to support Basic collections + ["uni: String collection", AW_Default_String, "books", String, ALL.getValue()], + ["bi: default", AW_Default_Bi, "books", Book_BT_Default, ALL.getValue()], + ["bi: hasOne (with belongsTo)", AW_HasOne_Bi, "profile", Profile_BT, ALL.getValue()], + ["uni: hasOne (no belongsTo)", AW_HasOne_Uni, "passport", Passport, ALL.getValue()], + ["many-to-many (owning side)", Post, "tags", Tag_BT, SAVE_UPDATE.getValue()], ["many-to-many (circular subclass)", Dog, "animals", Mammal, SAVE_UPDATE.getValue()], - ["many-to-many (inverse side)" , Tag_BT , "posts" , Post , NONE.getValue()], + ["many-to-many (inverse side)", Tag_BT, "posts", Post, NONE.getValue()], ["many-to-many (circular superclass)", Mammal, "dogs", Dog, NONE.getValue()], - ["many-to-one (belongsTo)" , Book_BT_Default , "author" , AW_Default_Bi , NONE.getValue()], - ["many-to-one (unidirectional)" , A , "manyToOne", ManyToOne , SAVE_UPDATE.getValue()], - ["many-to-one (bidirectional but superclass)" , Bird , "canary" , Canary , NONE.getValue()], + ["many-to-one (belongsTo)", Book_BT_Default, "author", AW_Default_Bi, NONE.getValue()], + ["many-to-one (unidirectional)", A, "manyToOne", ManyToOne, SAVE_UPDATE.getValue()], + ["many-to-one (bidirectional but superclass)", Bird, "canary", Canary, NONE.getValue()], -// --- Additional Hibernate 6+ specific scenarios --- + // --- Additional Hibernate 6+ specific scenarios --- ["uni: hasMany with explicit none", AW_None_Uni, "books", BookUni, NONE.getValue()], - ["bi: hasOne default conservative", AW_HasOne_Default, "profile", Profile_Default , ALL.getValue()], - ["orphan removal scenario" , AW_OrphanRemoval , "books", Book_Orphan , ALL_DELETE_ORPHAN.getValue()], + ["bi: hasOne default conservative", AW_HasOne_Default, "profile", Profile_Default, ALL.getValue()], + ["orphan removal scenario", AW_OrphanRemoval, "books", Book_Orphan, ALL_DELETE_ORPHAN.getValue()], // --- Map Association Scenarios --- ["map with belongsTo", ImpliedMapParent_All, "settings", ImpliedMapChild_All, ALL.getValue()], @@ -91,11 +83,7 @@ class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { ["embedded association", EOwner, "address", EAddress, ALL.getValue()] ] - - @Shared - CascadeBehaviorFetcher fetcher = new CascadeBehaviorFetcher() - - + @Shared CascadeBehaviorFetcher fetcher = new CascadeBehaviorFetcher() @Unroll void "test cascade behavior fetcher for #description"() { @@ -107,7 +95,6 @@ class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { when: "Getting the cascade behavior" def result = null def thrownException = null - try { result = fetcher.getCascadeBehaviour(testProperty) } catch (Exception e) { @@ -116,140 +103,191 @@ class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec { then: "The result matches the expectation" if (expectation instanceof Class && Exception.isAssignableFrom(expectation)) { - // Expecting an exception - if (thrownException == null) { - println "Error for description: '${description}'. Expected ${expectation.simpleName} to be thrown but no exception was thrown." - } else if (!expectation.isAssignableFrom(thrownException.class)) { - println "Error for description: '${description}'. Expected ${expectation.simpleName} but got ${thrownException.class.simpleName}." - } assert thrownException != null assert expectation.isAssignableFrom(thrownException.class) } else { - // Expecting a string result - if (thrownException != null) { - println "Error for description: '${description}'. Unexpected exception thrown: ${thrownException?.message}" - thrownException.printStackTrace() - } assert thrownException == null - if (result != expectation) { - println "Error for description: '${description}'. Expected cascade behavior '${expectation}' but got '${result}'." - } assert result == expectation } where: [description, ownerClass, associationName, childClass, expectation] << cascadeMetadataTestData } +} +// --- Test Domain Classes --- +@Entity class BookUni { String title } +@Entity +class AW_All_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'all' } +} +@Entity +class AW_SaveUpdate_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'persist,merge' } } -// --- Test Domain Classes for Various Scenarios --- -// Naming Convention: -// AW_ = AuthorWith... -// _Uni = Unidirectional hasMany (child has no belongsTo) -// _Bi = Bidirectional hasMany (child has a belongsTo) -// _BT = Suffix for a child class that has a `belongsTo` +@Entity +class AW_Merge_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'merge' } +} -// --- One-to-Many: Unidirectional --- -@Entity class BookUni { String title } +@Entity +class AW_Delete_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'delete' } +} -@Entity class AW_All_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'all' } } -@Entity class AW_SaveUpdate_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'persist,merge' } } -@Entity class AW_Merge_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'merge' } } -@Entity class AW_Delete_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'delete' } } -@Entity class AW_Lock_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'lock' } } -@Entity class AW_Replicate_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'replicate' } } -@Entity class AW_Evict_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'evict' } } -@Entity class AW_Persist_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'persist' } } -@Entity class AW_Invalid_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'invalid-string' } } +@Entity +class AW_Lock_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'lock' } +} -@Entity class AW_Default_Uni { static hasMany = [books: BookUni] } -class Buffalo{} -@Entity class AW_Default_String { String title; static hasMany = [books: Buffalo]} -@Entity class Book_BT_Default { String title; static belongsTo = [author: AW_Default_Bi] } -@Entity class AW_Default_Bi { static hasMany = [books: Book_BT_Default] } +@Entity +class AW_Replicate_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'replicate' } +} @Entity -class A { - ManyToOne manyToOne +class AW_Evict_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'evict' } } + @Entity -class ManyToOne { +class AW_Persist_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'persist' } } +@Entity +class AW_Invalid_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'invalid-string' } +} +@Entity class AW_Default_Uni { static hasMany = [books: BookUni] } +// FIX: Replaced class Buffalo with String to test Basic collections properly +@Entity +class AW_Default_String { + String title + static hasMany = [books: String] +} +@Entity +class Book_BT_Default { + String title + static belongsTo = [author: AW_Default_Bi] +} + +@Entity class AW_Default_Bi { static hasMany = [books: Book_BT_Default] } + +@Entity class A { ManyToOne manyToOne } +@Entity class ManyToOne { } -// --- One-to-One --- @Entity class Passport { String passportNumber } -@Entity class AW_HasOne_Uni { static hasOne = [passport: Passport] } // Unidirectional +@Entity class AW_HasOne_Uni { static hasOne = [passport: Passport] } -@Entity class Profile_BT { String bio; static belongsTo = [author: AW_HasOne_Bi] } -@Entity class AW_HasOne_Bi { static hasOne = [profile: Profile_BT] } // Bidirectional +@Entity +class Profile_BT { + String bio + static belongsTo = [author: AW_HasOne_Bi] +} -// --- Many-to-Many --- -@Entity class Post { String content; static hasMany = [tags: Tag_BT] } -@Entity class Tag_BT { String name; static hasMany = [posts: Post]; static belongsTo = Post } -@Entity class Mammal { String name; static hasMany = [dogs: Dog]} -@Entity class Dog extends Mammal { String foo; static hasMany = [animals: Mammal] } +@Entity class AW_HasOne_Bi { static hasOne = [profile: Profile_BT] } +@Entity +class Post { + String content + static hasMany = [tags: Tag_BT] +} + +@Entity +class Tag_BT { + String name + static hasMany = [posts: Post] + static belongsTo = Post +} + +@Entity class Mammal { String name; static hasMany = [dogs: Dog] } +@Entity class Dog extends Mammal { String foo; static hasMany = [animals: Mammal] } @Entity class Bird { String title; static belongsTo = [canary: Canary] } @Entity class Canary { static hasMany = [birds: Bird] } -@Entity class AW_None_Uni { static hasMany = [books: BookUni]; static mapping = { books cascade: 'none' } } -@Entity class Profile_Default { String bio; static belongsTo = [author: AW_HasOne_Default] } +@Entity +class AW_None_Uni { + static hasMany = [books: BookUni] + static mapping = { books cascade: 'none' } +} + +@Entity +class Profile_Default { + String bio + static belongsTo = [author: AW_HasOne_Default] +} + @Entity class AW_HasOne_Default { static hasOne = [profile: Profile_Default] } -@Entity class Book_Orphan { String title; static belongsTo = [author: AW_OrphanRemoval] } -@Entity class AW_OrphanRemoval { static hasMany = [books: Book_Orphan]; static mapping = { books cascade: 'all-delete-orphan' } } -// --- Map Association Scenarios --- -@Entity class ImpliedMapParent_All { +@Entity +class Book_Orphan { + String title + static belongsTo = [author: AW_OrphanRemoval] +} + +@Entity +class AW_OrphanRemoval { + static hasMany = [books: Book_Orphan] + static mapping = { books cascade: 'all-delete-orphan' } +} + +@Entity +class ImpliedMapParent_All { static hasMany = [settings: ImpliedMapChild_All] Map<String, ImpliedMapChild_All> settings } -@Entity class ImpliedMapChild_All { + +@Entity +class ImpliedMapChild_All { String value static belongsTo = [parent: ImpliedMapParent_All] } -@Entity class ImpliedMapParent_SaveUpdate { + +@Entity +class ImpliedMapParent_SaveUpdate { static hasMany = [settings: ImpliedMapChild_SaveUpdate] Map<String, ImpliedMapChild_SaveUpdate> settings } -@Entity class ImpliedMapChild_SaveUpdate { String value } +@Entity class ImpliedMapChild_SaveUpdate { String value } -// --- Composite ID Scenario --- @Entity class CompositeIdParent { Long id String name static hasMany = [children: CompositeIdManyToOne] } + @Entity class CompositeIdManyToOne implements Serializable { String name CompositeIdParent parent - - static mapping = { - id composite: ['name', 'parent'] - } - + static mapping = { id composite: ['name', 'parent'] } static belongsTo = [parent: CompositeIdParent] } -// --- Embedded Association Scenario --- @Entity class EOwner { EAddress address static embedded = ['address'] } -@Embeddable -class EAddress { - String street -} +@Embeddable class EAddress { String street } \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy index 0d457d47fc..d26c61c0db 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy @@ -90,6 +90,54 @@ class ColumnConfigToColumnBinderSpec extends Specification { !column.unique } + def "column config honors uniqueness property when set to a string (named group)"() { + given: + def columnConfig = new ColumnConfig(unique: "group1") + PropertyConfig mappedForm = new PropertyConfig(unique: "group1") + + when: + binder.bindColumnConfigToColumn(column, columnConfig, mappedForm) + + then: + !column.unique // Should be false because it's handled via unique groups in Hibernate + } + + def "column config honors uniqueness property when set to a list (composite groups)"() { + given: + def columnConfig = new ColumnConfig(unique: ["group1", "group2"]) + PropertyConfig mappedForm = new PropertyConfig(unique: ["group1", "group2"]) + + when: + binder.bindColumnConfigToColumn(column, columnConfig, mappedForm) + + then: + !column.unique + } + + def "column config honors uniqueness property when set to boolean true"() { + given: + def columnConfig = new ColumnConfig(unique: true) + PropertyConfig mappedForm = new PropertyConfig(unique: true) + + when: + binder.bindColumnConfigToColumn(column, columnConfig, mappedForm) + + then: + column.unique + } + + def "column config honors uniqueness property when set to boolean false"() { + given: + def columnConfig = new ColumnConfig(unique: false) + PropertyConfig mappedForm = new PropertyConfig(unique: false) + + when: + binder.bindColumnConfigToColumn(column, columnConfig, mappedForm) + + then: + !column.unique + } + def "column config honors uniqueness property when mappedForm is empty"() { given: def columnConfig = new ColumnConfig() diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy index aaca72d0d3..3fcdd2777d 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy @@ -20,20 +20,19 @@ package org.grails.orm.hibernate.cfg.domainbinding.hibernate import grails.gorm.annotation.Entity import grails.gorm.specs.HibernateGormDatastoreSpec -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec { - def setupSpec() { - manager.addAllDomainClasses([HTMPBook, HTMPAuthor, HTMPAuthorCustom, HTMPStudent, HTMPCourse]) - } + // Removed setupSpec to prevent loading all entities at once void "resolveJoinTableForeignKeyColumnName derives name from associated entity when no explicit config"() { - given: - def authorEntity = mappingContext.getPersistentEntity(HTMPAuthor.name) - HibernateToManyProperty property = (HibernateToManyProperty) authorEntity.getPropertyByName("books") + given: "Register only entities for this specific test" + def property = createTestHibernateToManyProperty(HTMPAuthor, "books") def namingStrategy = getGrailsDomainBinder().namingStrategy + and: "Trigger Hibernate First Pass" + hibernateFirstPass() + when: String columnName = property.resolveJoinTableForeignKeyColumnName(namingStrategy) @@ -42,11 +41,13 @@ class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec { } void "resolveJoinTableForeignKeyColumnName uses explicit join table column name when configured"() { - given: - def authorEntity = mappingContext.getPersistentEntity(HTMPAuthorCustom.name) - HibernateToManyProperty property = (HibernateToManyProperty) authorEntity.getPropertyByName("books") + given: "Register only entities for this specific test" + def property = createTestHibernateToManyProperty(HTMPAuthorCustom, "books") def namingStrategy = getGrailsDomainBinder().namingStrategy + and: "Trigger Hibernate First Pass" + hibernateFirstPass() + when: String columnName = property.resolveJoinTableForeignKeyColumnName(namingStrategy) @@ -55,15 +56,73 @@ class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec { } void "isAssociationColumnNullable returns false for ManyToMany"() { + given: "Register only entities for this specific test" + createPersistentEntity(HTMPCourse) // Course is needed because Student refers to it + def studentProp = createTestHibernateToManyProperty(HTMPStudent, "courses") + when: - def studentEntity = mappingContext.getPersistentEntity(HTMPStudent.name) - def coursesProp = studentEntity.getPropertyByName("courses") + hibernateFirstPass() then: - !coursesProp.isAssociationColumnNullable() + !studentProp.isAssociationColumnNullable() + } + + void "test index column configuration"() { + given: "Register the HTMPOrder entity using the helper" + def property = createTestHibernateToManyProperty(HTMPOrder, "items") + def namingStrategy = getGrailsDomainBinder().namingStrategy + + and: "Trigger Hibernate First Pass" + hibernateFirstPass() + + expect: "The index column name and type are resolved from the column list" + verifyAll(property) { + getIndexColumnName(namingStrategy) == "item_idx" + getIndexColumnType("integer") == "integer" + } + } + + void "test index column configuration with map"() { + given: + def property = createTestHibernateToManyProperty(HTMPOrderMap, "items") + def namingStrategy = getGrailsDomainBinder().namingStrategy + + and: "Trigger Hibernate First Pass" + hibernateFirstPass() + + expect: + verifyAll(property) { + getIndexColumnName(namingStrategy) == "map_idx" + getIndexColumnType("integer") == "string" + } + } + + void "test index column configuration with closure"() { + given: + def property = createTestHibernateToManyProperty(HTMPOrderClosure, "items") + def namingStrategy = getGrailsDomainBinder().namingStrategy + + and: "Trigger Hibernate First Pass" + hibernateFirstPass() + + expect: + verifyAll(property) { + getIndexColumnName(namingStrategy) == "closure_idx" + getIndexColumnType("integer") == "long" + } + } + + /** + * Helper to register entity and return the property + */ + protected HibernateToManyProperty createTestHibernateToManyProperty(Class<?> domainClass, String propertyName) { + def entity = createPersistentEntity(domainClass) + return (HibernateToManyProperty) entity.getPropertyByName(propertyName) } } +// --- Supporting Entities --- + @Entity class HTMPBook { Long id @@ -74,7 +133,6 @@ class HTMPBook { class HTMPAuthor { Long id String name - Set<HTMPBook> books static hasMany = [books: HTMPBook] } @@ -82,7 +140,6 @@ class HTMPAuthor { class HTMPAuthorCustom { Long id String name - Set<HTMPBook> books static hasMany = [books: HTMPBook] static mapping = { books joinTable: [column: 'custom_book_fk'] @@ -93,7 +150,6 @@ class HTMPAuthorCustom { class HTMPStudent { Long id String name - Set<HTMPCourse> courses static hasMany = [courses: HTMPCourse] } @@ -101,6 +157,47 @@ class HTMPStudent { class HTMPCourse { Long id String title - Set<HTMPStudent> students static hasMany = [students: HTMPStudent] } + +import grails.persistence.Entity + +@Entity // Only if outside grails-app/domain +class HTMPOrder { + Long id + + List<String> items // Remove the = [] + + static hasMany = [items: String] + + static mapping = { + items joinTable: [ + name: "htmp_order_items", + key: "order_id", + column: "item_value" + ], index: "item_idx" // Defines the column for the List index + } +} + +@Entity +class HTMPOrderMap { + Long id + List<String> items + static hasMany = [items: String] + static mapping = { + items index: [column: 'map_idx', type: 'string'] + } +} + +@Entity +class HTMPOrderClosure { + Long id + List<String> items + static hasMany = [items: String] + static mapping = { + items index: { + column name: 'closure_idx' + type 'long' + } + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy index 6861fe269e..328eff9f83 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy @@ -19,6 +19,7 @@ package org.grails.orm.hibernate.cfg.domainbinding.secondpass + import grails.gorm.annotation.Entity import grails.gorm.specs.HibernateGormDatastoreSpec import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty @@ -69,16 +70,43 @@ class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { return property } - def "resolveAssociatedClass throws MappingException when property has no associated entity"() { - given: - def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty - def spiedProperty = Spy(property) - spiedProperty.getHibernateAssociatedEntity() >> null + def "bindCollectionSecondPass succeeds for Basic String collection"() { + given: "An entity with a basic String collection" + def property = createTestHibernateToManyProperty(HTMPOrder, "items") as HibernateToManyProperty - when: - binder.resolveAssociatedClass(spiedProperty, [:]) + and: "We trigger the first pass mapping" + hibernateFirstPass() + + expect: "The Hibernate collection object is now initialized" + property.getCollection() != null + + when: "Binding second pass" + binder.bindCollectionSecondPass(property, [:]) then: + noExceptionThrown() + } + + def "resolveAssociatedClass throws MappingException when entity association is missing from persistentClasses"() { + given: "A standard entity association (not a Basic collection)" + def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty + + when: "Attempting to resolve associated class with an empty map" + binder.resolveAssociatedClass(property, [:]) + + then: "A MappingException is thrown because this is a real entity relationship" + def ex = thrown(org.hibernate.MappingException) + ex.message.contains("items") + } + + def "resolveAssociatedClass throws MappingException when entity association has no class in persistentClasses"() { + given: "An entity association (not a Basic collection)" + def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty + + when: "Attempting to resolve associated class with an empty map" + binder.resolveAssociatedClass(property, [:]) + + then: "A MappingException is thrown because this is a real entity association" def ex = thrown(org.hibernate.MappingException) ex.message.contains("items") } @@ -124,3 +152,9 @@ class CSPBAssociatedItem { CSPBTestEntityWithMany parent static belongsTo = [parent: CSPBTestEntityWithMany] } +@Entity +class HTMPOrder { + Long id + List<String> items = [] + static hasMany = [items: String] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy index b912873e8a..1e1780dd73 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy @@ -51,8 +51,8 @@ import org.hibernate.mapping.BasicValue import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher -import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator +import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder import org.grails.orm.hibernate.cfg.domainbinding.binder.SubClassBinder import org.grails.orm.hibernate.cfg.domainbinding.binder.SubclassMappingBinder @@ -174,8 +174,8 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { org.apache.grails.data.testing.tck.domains.Pet, org.apache.grails.data.testing.tck.domains.Person, org.apache.grails.data.testing.tck.domains.PetType, - MSBAuthor, - MSBBook + MapSPBAuthor, + MapSPBBook ]) } @@ -189,22 +189,22 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { def collectionBinder = binders.collectionBinder def mapBinder = collectionBinder.mapSecondPassBinder - def authorEntity = getPersistentEntity(MSBAuthor) as GrailsHibernatePersistentEntity - def bookEntity = getPersistentEntity(MSBBook) as GrailsHibernatePersistentEntity + def authorEntity = getPersistentEntity(MapSPBAuthor) as GrailsHibernatePersistentEntity + def bookEntity = getPersistentEntity(MapSPBBook) as GrailsHibernatePersistentEntity def booksProp = authorEntity.getPropertyByName("books") as HibernateToManyProperty def rootClass = new RootClass(metadataBuildingContext) rootClass.setEntityName(authorEntity.name) rootClass.setClassName(authorEntity.name) rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "MSB_AUTHOR", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext)) collector.addEntityBinding(rootClass) def bookRootClass = new RootClass(metadataBuildingContext) bookRootClass.setEntityName(bookEntity.name) bookRootClass.setClassName(bookEntity.name) bookRootClass.setJpaEntityName(bookEntity.name) - bookRootClass.setTable(collector.addTable(null, null, "MSB_BOOK", null, false, metadataBuildingContext)) + bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext)) collector.addEntityBinding(bookRootClass) def persistentClasses = [ @@ -239,27 +239,27 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { def collectionBinder = binders.collectionBinder def mapBinder = collectionBinder.mapSecondPassBinder - def authorEntity = getPersistentEntity(MSBAuthor) as GrailsHibernatePersistentEntity - def bookEntity = getPersistentEntity(MSBBook) as GrailsHibernatePersistentEntity + def authorEntity = getPersistentEntity(MapSPBAuthor) as GrailsHibernatePersistentEntity + def bookEntity = getPersistentEntity(MapSPBBook) as GrailsHibernatePersistentEntity def booksProp = authorEntity.getPropertyByName("books") as HibernateToManyProperty def rootClass = new RootClass(metadataBuildingContext) rootClass.setEntityName(authorEntity.name) rootClass.setClassName(authorEntity.name) rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "MSB_AUTHOR", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext)) collector.addEntityBinding(rootClass) def bookRootClass = new RootClass(metadataBuildingContext) bookRootClass.setEntityName(bookEntity.name) bookRootClass.setClassName(bookEntity.name) bookRootClass.setJpaEntityName(bookEntity.name) - bookRootClass.setTable(collector.addTable(null, null, "MSB_BOOK", null, false, metadataBuildingContext)) + bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext)) collector.addEntityBinding(bookRootClass) def persistentClasses = [ (authorEntity.name): rootClass, - (MSBBook.name): bookRootClass + (MapSPBBook.name): bookRootClass ] def map = new org.hibernate.mapping.Map(metadataBuildingContext, rootClass) @@ -267,7 +267,7 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { map.setCollectionTable(rootClass.getTable()) def element = new org.hibernate.mapping.ManyToOne(metadataBuildingContext, map.getCollectionTable()) - element.setReferencedEntityName(MSBBook.name) + element.setReferencedEntityName(MapSPBBook.name) map.setElement(element) booksProp.setCollection(map) @@ -284,17 +284,19 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { } @grails.gorm.annotation.Entity -class MSBAuthor { +class MapSPBAuthor { Long id - Map<String, MSBBook> books - static hasMany = [books: MSBBook] + Map<String, MapSPBBook> books + static hasMany = [books: MapSPBBook] static mapping = { - books index: 'BOOK_TITLE' + books index: { + column 'books_idx' + } } } @grails.gorm.annotation.Entity -class MSBBook { +class MapSPBBook { Long id String title }
