This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 439fd2a627899609529b1ef582dcafb38347cefa Author: Walter B Duque de Estrada <[email protected]> AuthorDate: Thu Jan 15 10:53:47 2026 -0600 update progress --- .../core/HIBERNATE7-UPGRADE-PROGRESS.md | 18 +++ .../hibernate/cfg/AbstractGrailsDomainBinder.java | 78 ----------- .../orm/hibernate/cfg/GrailsDomainBinder.java | 153 ++++++++++----------- .../hibernate/cfg/HibernatePersistentEntity.java | 19 +++ .../cfg/domainbinding/RootMappingFetcher.java | 19 +++ .../hibernate/query/GrailsHibernateQueryUtils.java | 11 +- .../specs/proxy/Hibernate7GroovyProxySpec.groovy | 1 + .../domainbinding/RootMappingFetcherSpec.groovy | 60 ++++++++ .../mapping/model/PersistentProperty.java | 11 ++ .../datastore/mapping/model/types/Association.java | 4 + 10 files changed, 205 insertions(+), 169 deletions(-) diff --git a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md index 1455485455..bec594ac79 100644 --- a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md +++ b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md @@ -59,6 +59,24 @@ This document summarizes the approaches taken, challenges encountered, and futur - **Issue:** Hibernate 7 proxies no longer implement `isInitialized()` or `getInitialized()` directly on the proxy object. - **Solution:** Switched to `Hibernate.isInitialized(proxy)`. +## Strategy for GrailsDomainBinder Refactoring + +### Goal +To decompose the monolithic `GrailsDomainBinder` (over 2000 lines) into smaller, specialized binder classes within the `org.grails.orm.hibernate.cfg.domainbinding` package. This improves maintainability and enables unit testing of specific binding logic. + +### Refactoring Pattern +Each new binder should follow this structure: +1. **Dependencies as Fields:** Define `private final` fields for all dependent binders and utility classes. +2. **Public Constructor:** A constructor that takes essential state (e.g., `PersistentEntityNamingStrategy`) and initializes simple dependencies internally. +3. **Protected Constructor for Testing:** A second constructor that accepts all dependencies as arguments. This allows unit tests to inject mocks for all collaborating classes. +4. **Core Method:** A public method that contains the logic previously held in `GrailsDomainBinder` (e.g., `bindCollectionSecondPass`). + +### Testing Strategy +Unit tests should be created for each new binder class (e.g., `CollectionBinderSpec`). These tests should: +- Use the protected constructor to inject mocks. +- Verify interactions with dependent binders using Spock's `Mock()` and `1 * ...` syntax. +- Ensure that the complex logic of `GrailsDomainBinder` is covered by isolated unit tests rather than relying solely on integration tests. + ## Future Steps 1. **Resolve Proxy Initialization:** Determine why proxies are returning as initialized in `Hibernate6GroovyProxySpec`. Investigate if Hibernate 7's bytecode enhancement or ByteBuddy factory settings are interfering. diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java deleted file mode 100644 index f2093bdeaf..0000000000 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright 2004-2005 the original author or authors. - * - * Licensed 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 - * - * http://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 org.grails.datastore.mapping.model.PersistentEntity; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Handles the binding Grails domain classes and properties to the Hibernate runtime meta model. - * Based on the HbmBinder code in Hibernate core and influenced by AnnotationsBinder. - * - * @author Graeme Rocher - * @since 0.1 - */ -public abstract class AbstractGrailsDomainBinder { - protected static final Map<Class<?>, Mapping> MAPPING_CACHE = new HashMap<>(); - - - /** - * Obtains a mapping object for the given domain class nam - * - * @param theClass The domain class in question - * @return A Mapping object or null - */ - public static Mapping getMapping(Class<?> theClass) { - return theClass == null ? null : MAPPING_CACHE.get(theClass); - } - - /** - * Obtains a mapping object for the given domain class nam - * - * @param theClass The domain class in question - */ - public static void cacheMapping(Class<?> theClass, Mapping mapping) { - MAPPING_CACHE.put(theClass, mapping); - } - - /** - * Obtains a mapping object for the given domain class nam - * - * @param domainClass The domain class in question - * @return A Mapping object or null - */ - public static Mapping getMapping(PersistentEntity domainClass) { - return domainClass == null ? null : MAPPING_CACHE.get(domainClass.getJavaClass()); - } - - - public static void clearMappingCache() { - MAPPING_CACHE.clear(); - } - - public static void clearMappingCache(Class<?> theClass) { - String className = theClass.getName(); - for(Iterator<Map.Entry<Class<?>, Mapping>> it = MAPPING_CACHE.entrySet().iterator(); it.hasNext();) { - Map.Entry<Class<?>, Mapping> entry = it.next(); - if (className.equals(entry.getKey().getName())) { - it.remove(); - } - } - } -} - diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java index 8d03295a55..4a36a3e597 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java @@ -15,9 +15,8 @@ package org.grails.orm.hibernate.cfg; import groovy.lang.Closure; -import jakarta.persistence.Entity; + import org.grails.datastore.mapping.core.connections.ConnectionSource; -import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; import org.grails.datastore.mapping.model.DatastoreConfigurationException; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.PersistentProperty; @@ -45,7 +44,6 @@ import org.hibernate.boot.internal.RootMappingDefaults; import org.hibernate.boot.model.TypeContributions; import org.hibernate.boot.model.TypeContributor; import org.hibernate.boot.model.internal.BinderHelper; -import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.boot.spi.AdditionalMappingContributions; import org.hibernate.boot.spi.AdditionalMappingContributor; import org.hibernate.boot.spi.InFlightMetadataCollector; @@ -118,6 +116,7 @@ public class GrailsDomainBinder { public static final String FOREIGN_KEY_SUFFIX = "_id"; + protected static final Map<Class<?>, Mapping> MAPPING_CACHE = new HashMap<>(); private static final String STRING_TYPE = "string"; private static final String EMPTY_PATH = ""; public static final char UNDERSCORE = '_'; @@ -149,6 +148,59 @@ public class GrailsDomainBinder private PersistentEntityNamingStrategy namingStrategy; private MetadataBuildingContext metadataBuildingContext; + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(Class<?> theClass) { + return theClass == null ? null : MAPPING_CACHE.get(theClass); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMappingValidated(Class<?> theClass) { + return ofNullable(getMapping(theClass)).orElseThrow(); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + */ + public static void cacheMapping(Class<?> theClass, Mapping mapping) { + MAPPING_CACHE.put(theClass, mapping); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param domainClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(PersistentEntity domainClass) { + return domainClass == null ? null : MAPPING_CACHE.get(domainClass.getJavaClass()); + } + + public static void clearMappingCache() { + MAPPING_CACHE.clear(); + } + + public static void clearMappingCache(Class<?> theClass) { + String className = theClass.getName(); + for(Iterator<Map.Entry<Class<?>, Mapping>> it = MAPPING_CACHE.entrySet().iterator(); it.hasNext();) { + Map.Entry<Class<?>, Mapping> entry = it.next(); + if (className.equals(entry.getKey().getName())) { + it.remove(); + } + } + } + public JdbcEnvironment getJdbcEnvironment() { return metadataBuildingContext.getMetadataCollector().getDatabase().getJdbcEnvironment(); @@ -212,24 +264,14 @@ public class GrailsDomainBinder , rootMappingDefaults ); - filterHibernateEntities(hibernateMappingContext.getHibernatePersistentEntities()) + hibernateMappingContext.getHibernatePersistentEntities().stream() + .filter(this::isForGrailsDomainMapping) .forEach(hibernatePersistentEntity -> bindRoot(hibernatePersistentEntity, metadataCollector, sessionFactoryName)); } - private List<HibernatePersistentEntity> filterHibernateEntities(java.util.Collection<HibernatePersistentEntity> persistentEntities) { - return persistentEntities.stream() - .filter(this::isNotAnnotatedEntity) - .filter(this::usesConnectionSource) - .filter(HibernatePersistentEntity::isRoot).toList(); - } - - private boolean usesConnectionSource(HibernatePersistentEntity persistentEntity) { - return ConnectionSourcesSupport.usesConnectionSource(persistentEntity, dataSourceName); - } - - private boolean isNotAnnotatedEntity(HibernatePersistentEntity persistentEntity) { - return !persistentEntity.getJavaClass().isAnnotationPresent(Entity.class); + private boolean isForGrailsDomainMapping(HibernatePersistentEntity persistentEntity) { + return persistentEntity.forGrailsDomainMapping(dataSourceName); } @@ -361,7 +403,7 @@ public class GrailsDomainBinder PersistentClass referenced = mappings.getEntityBinding(entityName); Class<?> mappedClass = referenced.getMappedClass(); - Mapping m = getMapping(mappedClass); + Mapping m = getMappingValidated(mappedClass); boolean compositeIdProperty = m.isCompositeIdProperty(property.getInverseSide()); if (!compositeIdProperty) { @@ -439,11 +481,11 @@ public class GrailsDomainBinder // Configure one-to-many if (collection.isOneToMany()) { - Mapping m = getRootMapping(referenced); + Mapping m = new RootMappingFetcher().getRootMapping(referenced); boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); if (referenced != null && !referenced.isRoot() && !tablePerSubclass) { - Mapping rootMapping = getRootMapping(referenced); + Mapping rootMapping = new RootMappingFetcher().getRootMapping(referenced); //TODO FIXME String discriminatorColumnName = JPA_DEFAULT_DISCRIMINATOR_TYPE; @@ -496,7 +538,7 @@ public class GrailsDomainBinder } } - if (isSorted(property)) { + if (property.isSorted()) { collection.setSorted(true); } @@ -530,7 +572,7 @@ public class GrailsDomainBinder } // if we have a many-to-many - if (isManyToMany || isBidirectionalOneToManyMap(property)) { + if (isManyToMany || property.isBidirectionalOneToManyMap()) { PersistentProperty otherSide = property.getInverseSide(); if (property.isBidirectional()) { @@ -682,7 +724,7 @@ public class GrailsDomainBinder discriminator = discriminatorConfig.getValue(); } } - Mapping rootMapping = getRootMapping(domainClass); + Mapping rootMapping = new RootMappingFetcher().getRootMapping(domainClass); String quote = "'"; if (rootMapping != null && rootMapping.getDatasources() != null) { DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); @@ -698,19 +740,6 @@ public class GrailsDomainBinder return theSet; } - private Mapping getRootMapping(PersistentEntity referenced) { - return Optional.of(referenced) - .map(PersistentEntity::getRootEntity) - .filter(HibernatePersistentEntity.class::isInstance) - .map(HibernatePersistentEntity.class::cast) - .map(HibernatePersistentEntity::getMappedForm) - .orElse(null); - } - - private boolean isBidirectionalOneToManyMap(Association property) { - return Map.class.isAssignableFrom(property.getType()) && property.isBidirectional(); - } - private void bindCollectionWithJoinTable(ToMany property, InFlightMetadataCollector mappings, Collection collection, PropertyConfig config, String sessionFactoryBeanName) { @@ -947,16 +976,6 @@ public class GrailsDomainBinder } } - /** - * Establish whether a collection property is sorted - * - * @param property The property - * @return true if sorted - */ - private boolean isSorted(PersistentProperty property) { - return SortedSet.class.isAssignableFrom(property.getType()); - } - /** * Binds a many-to-many relationship. A many-to-many consists of * - a key (a DependentValue) @@ -1100,13 +1119,6 @@ public class GrailsDomainBinder } - private PhysicalNamingStrategy getPhysicalNamingStrategy(String sessionFactoryBeanName) { - return NAMING_STRATEGY_PROVIDER.getPhysicalNamingStrategy(sessionFactoryBeanName); - } - - - - /** * Binds a Grails domain class to the Hibernate runtime meta model * @@ -1128,7 +1140,7 @@ public class GrailsDomainBinder try { final Mapping m = new HibernateEntityWrapper().getMappedForm(domainClass); trackCustomCascadingSaves(m, domainClass.getPersistentProperties()); - AbstractGrailsDomainBinder.cacheMapping(domainClass.getJavaClass(), m); + cacheMapping(domainClass.getJavaClass(), m); } catch (Exception e) { throw new DatastoreConfigurationException("Error evaluating ORM mappings block for domain [" + domainClass.getName() + "]: " + e.getMessage(), e); @@ -1147,38 +1159,11 @@ public class GrailsDomainBinder PropertyConfig propConf = mapping.getPropertyConfig(property.getName()); if (propConf != null && propConf.getCascade() != null) { - propConf.setExplicitSaveUpdateCascade(isSaveUpdateCascade(propConf.getCascade())); + propConf.setExplicitSaveUpdateCascade(CascadeBehavior.isSaveUpdate(propConf.getCascade())); } } } - /** - * Check if a save-update cascade is defined within the Hibernate cascade properties string. - * @param cascade The string containing the cascade properties. - * @return True if save-update or any other cascade property that encompasses those is present. - */ - protected boolean isSaveUpdateCascade(String cascade) { - return CascadeBehavior.isSaveUpdate(cascade); - } - - /** - * Obtains a mapping object for the given domain class nam - * - * @param theClass The domain class in question - * @return A Mapping object or null - */ - private static Mapping getMapping(Class<?> theClass) { - return Optional.ofNullable(AbstractGrailsDomainBinder.getMapping(theClass)).orElseThrow(); - } - - - public static void clearMappingCache() { - AbstractGrailsDomainBinder.clearMappingCache(); - } - - public static void clearMappingCache(Class<?> theClass) { - // no-op, here for compatibility - } /** * Binds a root class (one with no super classes) to the runtime meta model @@ -1223,7 +1208,7 @@ public class GrailsDomainBinder public PersistentEntityNamingStrategy getNamingStrategy() { if (namingStrategy == null) { - namingStrategy = new NamingStrategyWrapper(getPhysicalNamingStrategy(sessionFactoryName), getJdbcEnvironment()); + namingStrategy = new NamingStrategyWrapper(NAMING_STRATEGY_PROVIDER.getPhysicalNamingStrategy(sessionFactoryName), getJdbcEnvironment()); } return namingStrategy; } @@ -1285,7 +1270,7 @@ public class GrailsDomainBinder .stream() .filter(HibernatePersistentEntity.class::isInstance) .map(HibernatePersistentEntity.class::cast) - .filter(this::usesConnectionSource) + .filter(persistentEntity -> persistentEntity.usesConnectionSource(dataSourceName)) .filter(sub -> isChildEntity(sub, domainClass)) .forEach( sub -> bindSubClass(sub, parent, mappings, sessionFactoryBeanName, m)); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java index 29b07d9527..af8b90e4e6 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java @@ -15,10 +15,13 @@ */ package org.grails.orm.hibernate.cfg; +import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; import org.grails.datastore.mapping.model.*; import java.util.Optional; +import jakarta.persistence.Entity; + /** * Persistent entity implementation for Hibernate * @@ -63,4 +66,20 @@ public class HibernatePersistentEntity extends AbstractPersistentEntity<Mapping> public Mapping getMappedForm() { return Optional.ofNullable(getMapping()).map(ClassMapping::getMappedForm).orElse(null); } + + private boolean isAnnotatedEntity() { + return getJavaClass().isAnnotationPresent(Entity.class); + } + + public boolean usesConnectionSource(String dataSourceName) { + return ConnectionSourcesSupport.usesConnectionSource(this, dataSourceName); + } + + public boolean forGrailsDomainMapping(String dataSourceName) { + return !isAnnotatedEntity() + && usesConnectionSource(dataSourceName) + && isRoot(); + } + + } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcher.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcher.java new file mode 100644 index 0000000000..741e590689 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcher.java @@ -0,0 +1,19 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import java.util.Optional; + +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.orm.hibernate.cfg.HibernatePersistentEntity; +import org.grails.orm.hibernate.cfg.Mapping; + +public class RootMappingFetcher { + + public Mapping getRootMapping(PersistentEntity referenced) { + return Optional.ofNullable(referenced) + .map(PersistentEntity::getRootEntity) + .filter(HibernatePersistentEntity.class::isInstance) + .map(HibernatePersistentEntity.class::cast) + .map(HibernatePersistentEntity::getMappedForm) + .orElse(null); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java index 88fb486460..8195a0e9c0 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java @@ -2,7 +2,7 @@ package org.grails.orm.hibernate.query; import org.grails.datastore.mapping.config.Property; import org.grails.datastore.mapping.reflect.ClassUtils; -import org.grails.orm.hibernate.cfg.AbstractGrailsDomainBinder; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; import org.grails.orm.hibernate.cfg.Mapping; import org.grails.datastore.gorm.finders.DynamicFinder; import org.grails.datastore.mapping.model.PersistentEntity; @@ -11,23 +11,20 @@ import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.model.types.Embedded; import org.hibernate.FetchMode; import org.hibernate.FlushMode; -import org.hibernate.LockMode; import org.hibernate.query.Query; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.expression.SqmFunction; import org.springframework.core.convert.ConversionService; -import org.springframework.util.ReflectionUtils; import jakarta.persistence.LockModeType; import jakarta.persistence.criteria.*; -import java.lang.reflect.Method; + import java.util.Arrays; import java.util.Comparator; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Stream; /** @@ -89,7 +86,7 @@ public class GrailsHibernateQueryUtils { addOrderPossiblyNested(query, queryRoot, criteriaBuilder,entity, sort, order, ignoreCase); } } else if (useDefaultMapping) { - Mapping m = AbstractGrailsDomainBinder.getMapping(entity.getJavaClass()); + Mapping m = GrailsDomainBinder.getMapping(entity.getJavaClass()); if (m != null) { Map sortMap = m.getSort().getNamesAndDirections(); for (Object sort : sortMap.keySet()) { @@ -281,7 +278,7 @@ public class GrailsHibernateQueryUtils { * @param criteria The criteria */ private static void cacheCriteriaByMapping(Class<?> targetClass, Query criteria) { - Mapping m = AbstractGrailsDomainBinder.getMapping(targetClass); + Mapping m = GrailsDomainBinder.getMapping(targetClass); if (m != null && m.getCache() != null && m.getCache().getEnabled()) { criteria.setCacheable(true); } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/proxy/Hibernate7GroovyProxySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/proxy/Hibernate7GroovyProxySpec.groovy index 59c59c1145..21f070c6d4 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/proxy/Hibernate7GroovyProxySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/proxy/Hibernate7GroovyProxySpec.groovy @@ -28,6 +28,7 @@ class Hibernate7GroovyProxySpec extends GrailsDataTckSpec<GrailsDataHibernate7Tc id == location.id // Use the method on the proxy false == location.isInitialized() + false == org.hibernate.Hibernate.isInitialized(location) "UK" == location.code "United Kingdom - UK" == location.namedAndCode() diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcherSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcherSpec.groovy new file mode 100644 index 0000000000..1c55b84786 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/RootMappingFetcherSpec.groovy @@ -0,0 +1,60 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.orm.hibernate.cfg.HibernatePersistentEntity +import org.grails.orm.hibernate.cfg.Mapping +import spock.lang.Specification + +class RootMappingFetcherSpec extends Specification { + + RootMappingFetcher fetcher = new RootMappingFetcher() + + void "test getRootMapping returns null for null input"() { + expect: + fetcher.getRootMapping(null) == null + } + + void "test getRootMapping returns null for non-HibernatePersistentEntity"() { + given: + PersistentEntity entity = Mock(PersistentEntity) + entity.getRootEntity() >> entity + + expect: + fetcher.getRootMapping(entity) == null + } + + void "test getRootMapping returns mapping for HibernatePersistentEntity"() { + given: + HibernatePersistentEntity entity = Mock(HibernatePersistentEntity) + Mapping mapping = new Mapping() + entity.getRootEntity() >> entity + entity.getMappedForm() >> mapping + + expect: + fetcher.getRootMapping(entity) == mapping + } + + void "test getRootMapping returns root mapping for child HibernatePersistentEntity"() { + given: + HibernatePersistentEntity root = Mock(HibernatePersistentEntity) + HibernatePersistentEntity child = Mock(HibernatePersistentEntity) + Mapping rootMapping = new Mapping() + + child.getRootEntity() >> root + root.getMappedForm() >> rootMapping + + expect: + fetcher.getRootMapping(child) == rootMapping + } + + void "test getRootMapping returns null if root is not HibernatePersistentEntity"() { + given: + HibernatePersistentEntity child = Mock(HibernatePersistentEntity) + PersistentEntity root = Mock(PersistentEntity) + + child.getRootEntity() >> root + + expect: + fetcher.getRootMapping(child) == null + } +} diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java index d6f3a30551..5f9c35fce3 100644 --- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java +++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/PersistentProperty.java @@ -30,6 +30,7 @@ import org.grails.datastore.mapping.model.types.ToOne; import org.grails.datastore.mapping.reflect.EntityReflector; import java.util.Optional; +import java.util.SortedSet; import static java.util.Optional.ofNullable; @@ -120,4 +121,14 @@ public interface PersistentProperty<T extends Property> { return this instanceof ManyToMany || isUnidirectionalOneToMany()|| this instanceof Basic; } + /** + * Establish whether a collection property is sorted + * + * @param property The property + * @return true if sorted + */ + default boolean isSorted() { + return SortedSet.class.isAssignableFrom(this.getType()); + } + } diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/Association.java b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/Association.java index 98b226f6e4..0778786d0b 100644 --- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/Association.java +++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/Association.java @@ -369,4 +369,8 @@ public abstract class Association<T extends Property> extends AbstractPersistent .map(otherSide -> !this.isOwningSide() && otherSide.isOwningSide()) .orElse(false); } + + public boolean isBidirectionalOneToManyMap() { + return Map.class.isAssignableFrom(this.getType()) && this.isBidirectional(); + } }
