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 7b51b8b1a991fa2c4bf6e6cf91e0cf439bcae158 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Feb 6 20:30:35 2026 -0600 progress --- .../orm/hibernate/cfg/GrailsDomainBinder.java | 4 + .../cfg/domainbinding/CollectionBinder.java | 619 +------------------ ...Binder.java => CollectionSecondPassBinder.java} | 655 +++++++-------------- .../cfg/domainbinding/MapSecondPassBinder.java | 114 ++++ .../CollectionSecondPassBinderSpec.groovy | 81 +++ .../domainbinding/MapSecondPassBinderSpec.groovy | 83 +++ 6 files changed, 499 insertions(+), 1057 deletions(-) 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 67d5275179..1ee42fff8c 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 @@ -309,6 +309,10 @@ public class GrailsDomainBinder mappings.addEntityBinding(root); } + public String getMultiTenantFilterCondition(PersistentEntity referenced) { + return collectionBinder.getMultiTenantFilterCondition(referenced); + } + public PersistentEntityNamingStrategy getNamingStrategy() { if (namingStrategy == null) { namingStrategy = new NamingStrategyWrapper(NAMING_STRATEGY_PROVIDER.getPhysicalNamingStrategy(sessionFactoryName), getJdbcEnvironment()); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java index ff2995e9df..705ed48632 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java @@ -1,14 +1,11 @@ package org.grails.orm.hibernate.cfg.domainbinding; import jakarta.annotation.Nonnull; -import org.grails.datastore.mapping.model.DatastoreConfigurationException; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.PersistentProperty; -import org.grails.datastore.mapping.model.config.GormProperties; import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.model.types.Basic; import org.grails.datastore.mapping.model.types.ManyToMany; -import org.grails.datastore.mapping.model.types.TenantId; import org.grails.orm.hibernate.cfg.GrailsDomainBinder; import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity; import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty; @@ -18,50 +15,31 @@ import org.grails.orm.hibernate.cfg.Mapping; import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; import org.grails.orm.hibernate.cfg.PropertyConfig; import org.grails.orm.hibernate.cfg.ColumnConfig; -import org.grails.orm.hibernate.cfg.DiscriminatorConfig; -import org.grails.orm.hibernate.cfg.CacheConfig; import org.grails.orm.hibernate.cfg.JoinTable; -import org.grails.orm.hibernate.cfg.CompositeIdentity; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.GrailsCollectionSecondPass; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.ListSecondPass; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.MapSecondPass; import org.hibernate.FetchMode; import org.hibernate.MappingException; -import org.hibernate.boot.model.internal.BinderHelper; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; -import org.hibernate.mapping.Backref; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Column; import org.hibernate.mapping.Component; -import org.hibernate.mapping.DependantValue; import org.hibernate.mapping.IndexedCollection; -import org.hibernate.mapping.KeyValue; -import org.hibernate.mapping.ManyToOne; import org.hibernate.mapping.OneToMany; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; -import org.hibernate.mapping.Selectable; import org.hibernate.mapping.SimpleValue; -import org.hibernate.mapping.SingleTableSubclass; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; import org.hibernate.type.StandardBasicTypes; -import org.hibernate.type.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.StringTokenizer; import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.*; @@ -76,12 +54,16 @@ public class CollectionBinder { private final GrailsDomainBinder grailsDomainBinder; private final PersistentEntityNamingStrategy namingStrategy; private final ListSecondPassBinder listSecondPassBinder; + private final CollectionSecondPassBinder collectionSecondPassBinder; + private final MapSecondPassBinder mapSecondPassBinder; public CollectionBinder(MetadataBuildingContext metadataBuildingContext, GrailsDomainBinder grailsDomainBinder, PersistentEntityNamingStrategy namingStrategy) { this.metadataBuildingContext = metadataBuildingContext; this.grailsDomainBinder = grailsDomainBinder; this.namingStrategy = namingStrategy; this.listSecondPassBinder = new ListSecondPassBinder(metadataBuildingContext, this); + this.collectionSecondPassBinder = new CollectionSecondPassBinder(metadataBuildingContext, namingStrategy); + this.mapSecondPassBinder = new MapSecondPassBinder(metadataBuildingContext, namingStrategy, this); } /** @@ -150,168 +132,7 @@ public class CollectionBinder { public void bindCollectionSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { - PersistentClass associatedClass = null; - - if (LOG.isDebugEnabled()) - LOG.debug("Mapping collection: " - + collection.getRole() - + " -> " - + collection.getCollectionTable().getName()); - - PropertyConfig propConfig = property.getMappedForm(); - - PersistentEntity referenced = property.getAssociatedEntity(); - if (StringUtils.hasText(propConfig.getSort())) { - if (!property.isBidirectional() && (property instanceof org.grails.datastore.mapping.model.types.OneToMany)) { - throw new DatastoreConfigurationException("Default sort for associations ["+property.getOwner().getName()+"->" + property.getName() + - "] are not supported with unidirectional one to many relationships."); - } - if (referenced != null) { - PersistentProperty propertyToSortBy = referenced.getPropertyByName(propConfig.getSort()); - - String associatedClassName = property.getAssociatedEntity().getName(); - - associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); - if (associatedClass != null) { - collection.setOrderBy(buildOrderByClause(propertyToSortBy.getName(), associatedClass, collection.getRole(), - propConfig.getOrder() != null ? propConfig.getOrder() : "asc")); - } - } - } - - // Configure one-to-many - if (collection.isOneToMany()) { - - Mapping m = new RootMappingFetcher().getRootMapping(referenced); - boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); - - if (referenced != null && !referenced.isRoot() && !tablePerSubclass) { - Mapping rootMapping = new RootMappingFetcher().getRootMapping(referenced); - //TODO FIXME - String discriminatorColumnName = JPA_DEFAULT_DISCRIMINATOR_TYPE; - - if (rootMapping != null) { - DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); - if(discriminatorConfig != null) { - final ColumnConfig discriminatorColumn = discriminatorConfig.getColumn(); - if (discriminatorColumn != null) { - discriminatorColumnName = discriminatorColumn.getName(); - } - if (discriminatorConfig.getFormula() != null) { - discriminatorColumnName = discriminatorConfig.getFormula(); - } - } - } - //NOTE: this will build the set for the in clause if it has sublcasses - Set<String> discSet = buildDiscriminatorSet((GrailsHibernatePersistentEntity) referenced); - String inclause = String.join(",", discSet); - - collection.setWhere(discriminatorColumnName + " in (" + inclause + ")"); - } - - - OneToMany oneToMany = (OneToMany) collection.getElement(); - String associatedClassName = oneToMany.getReferencedEntityName(); - - associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); - // if there is no persistent class for the association throw exception - if (associatedClass == null) { - throw new MappingException("Association references unmapped class: " + oneToMany.getReferencedEntityName()); - } - - oneToMany.setAssociatedClass(associatedClass); - if (property.shouldBindWithForeignKey()) { - collection.setCollectionTable(associatedClass.getTable()); - } - - new CollectionForPropertyConfigBinder().bindCollectionForPropertyConfig(collection, propConfig); - } - - final boolean isManyToMany = property instanceof ManyToMany; - if(referenced != null && !isManyToMany && referenced.isMultiTenant()) { - String filterCondition = getMultiTenantFilterCondition(referenced); - if(filterCondition != null) { - if (property.isUnidirectionalOneToMany()) { - collection.addManyToManyFilter(GormProperties.TENANT_IDENTITY, filterCondition, true, Collections.emptyMap(), Collections.emptyMap()); - } else { - collection.addFilter(GormProperties.TENANT_IDENTITY, filterCondition, true, Collections.emptyMap(), Collections.emptyMap()); - } - } - } - - if (property.isSorted()) { - collection.setSorted(true); - } - - // setup the primary key references - DependantValue key = createPrimaryKeyValue(mappings, (GrailsHibernatePersistentProperty) property, collection, persistentClasses); - - // link a bidirectional relationship - if (property.isBidirectional()) { - - GrailsHibernatePersistentProperty otherSide = (GrailsHibernatePersistentProperty) property.getInverseSide(); - - if ((otherSide instanceof org.grails.datastore.mapping.model.types.ToOne) && property.shouldBindWithForeignKey()) { - - linkBidirectionalOneToMany(collection, associatedClass, key, otherSide); - - } else if ((otherSide instanceof ManyToMany) || Map.class.isAssignableFrom(property.getType())) { - - bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key); - - } - - } else { - - if (propConfig.hasJoinKeyMapping()) { - - String columnName = propConfig.getJoinTable().getKey().getName(); - - new SimpleValueColumnBinder().bindSimpleValue(key, "long", columnName, false); - - } else { - - bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key); - - } - - } - collection.setKey(key); - - // get cache config - if (propConfig != null) { - CacheConfig cacheConfig = propConfig.getCache(); - if (cacheConfig != null) { - collection.setCacheConcurrencyStrategy(cacheConfig.getUsage()); - } - } - - // if we have a many-to-many - if (isManyToMany || property.isBidirectionalOneToManyMap()) { - PersistentProperty otherSide = property.getInverseSide(); - - if (property.isBidirectional()) { - if (LOG.isDebugEnabled()) - LOG.debug("[CollectionBinder] Mapping other side " + otherSide.getOwner().getName() + "." + otherSide.getName() + " -> " + collection.getCollectionTable().getName() + " as ManyToOne"); - ManyToOne element = new ManyToOne(metadataBuildingContext, collection.getCollectionTable()); - bindManyToMany((Association)otherSide, element, mappings, sessionFactoryBeanName); - collection.setElement(element); - new CollectionForPropertyConfigBinder().bindCollectionForPropertyConfig(collection, propConfig); - if (property.isCircular()) { - collection.setInverse(false); - } - } else { - // TODO support unidirectional many-to-many - } - } else if (property.supportsJoinColumnMapping()) { - bindCollectionWithJoinTable(property, mappings, collection, propConfig, sessionFactoryBeanName); - - } else if (property.isUnidirectionalOneToMany()) { - // for non-inverse one-to-many, with a not-null fk, add a backref! - // there are problems with list and map mappings and join columns relating to duplicate key constraints - // TODO change this when HHH-1268 is resolved - bindUnidirectionalOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, mappings, collection); - } + collectionSecondPassBinder.bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); } public void bindListSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, @@ -321,68 +142,7 @@ public class CollectionBinder { public void bindMapSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { - bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); - - SimpleValue value = new BasicValue(metadataBuildingContext, map.getCollectionTable()); - - String type = ((GrailsHibernatePersistentProperty) property).getIndexColumnType("string"); - String columnName1 = getIndexColumnName(property); - new SimpleValueColumnBinder().bindSimpleValue(value, type, columnName1, true); - PropertyConfig mappedForm = property.getMappedForm(); - if (mappedForm.getIndexColumn() != null) { - Column column = new SimpleValueColumnFetcher().getColumnForSimpleValue(value); - ColumnConfig columnConfig = getSingleColumnConfig(mappedForm.getIndexColumn()); - new ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, columnConfig, mappedForm); - } - - if (!value.isTypeSpecified()) { - throw new MappingException("map index element must specify a type: " + map.getRole()); - } - map.setIndex(value); - - if(!(property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !(property instanceof ManyToMany)) { - - SimpleValue elt = new BasicValue(metadataBuildingContext, map.getCollectionTable()); - map.setElement(elt); - - Mapping mapping = null; - GrailsHibernatePersistentEntity domainClass = (GrailsHibernatePersistentEntity) property.getOwner(); - if (domainClass != null) { - mapping = domainClass.getMappedForm(); - } - String typeName = property.getTypeName(); - if (typeName == null ) { - - if(property instanceof Basic) { - Basic basic = (Basic) property; - typeName = basic.getComponentType().getName(); - } - } - if(typeName == null || typeName.equals(Object.class.getName())) { - typeName = StandardBasicTypes.STRING.getName(); - } - String columnName = getMapElementName(property, sessionFactoryBeanName); - new SimpleValueColumnBinder().bindSimpleValue(elt, typeName, columnName, false); - - elt.setTypeName(typeName); - } - - map.setInverse(false); - } - - private ColumnConfig getSingleColumnConfig(PropertyConfig propertyConfig) { - if (propertyConfig != null) { - List<ColumnConfig> columns = propertyConfig.getColumns(); - if (columns != null && !columns.isEmpty()) { - return columns.get(0); - } - } - return null; - } - - private PersistentClass getAssociatedClass(Map<?, ?> persistentClasses, HibernateToManyProperty property) { - String associatedClassName = property.getAssociatedEntity().getName(); - return (PersistentClass) persistentClasses.get(associatedClassName); + mapSecondPassBinder.bindMapSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); } private String getNameForPropertyAndPath(PersistentProperty property, String path) { @@ -435,342 +195,6 @@ public class CollectionBinder { return namingStrategy.resolveColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; } - private String getMapElementName(PersistentProperty property, String sessionFactoryBeanName) { - PropertyConfig pc = property instanceof GrailsHibernatePersistentProperty ghpp ? ghpp.getMappedForm() : new PropertyConfig(); - - if (hasJoinTableColumnNameMapping(pc)) { - return pc.getJoinTable().getColumn().getName(); - } - return namingStrategy.resolveColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_ELEMENT_COLUMN_NAME; - } - - private boolean hasJoinTableColumnNameMapping(PropertyConfig pc) { - return pc != null && pc.getJoinTable() != null && pc.getJoinTable().getColumn() != null && pc.getJoinTable().getColumn().getName() != null; - } - - public String getMultiTenantFilterCondition(PersistentEntity referenced) { - TenantId tenantId = referenced.getTenantId(); - if(tenantId != null) { - - String defaultColumnName = new DefaultColumnNameFetcher(namingStrategy).getDefaultColumnName(tenantId); - return ":tenantId = " + defaultColumnName; - } - else { - return null; - } - } - - @SuppressWarnings("unchecked") - private String buildOrderByClause(String hqlOrderBy, PersistentClass associatedClass, String role, String defaultOrder) { - String orderByString = null; - if (hqlOrderBy != null) { - List<String> properties = new ArrayList<>(); - List<String> ordering = new ArrayList<>(); - StringBuilder orderByBuffer = new StringBuilder(); - if (hqlOrderBy.length() == 0) { - //order by id - Iterator<?> it = associatedClass.getIdentifier().getSelectables().iterator(); - while (it.hasNext()) { - Selectable col = (Selectable) it.next(); - orderByBuffer.append(col.getText()).append(" asc").append(", "); - } - } - else { - StringTokenizer st = new StringTokenizer(hqlOrderBy, " ,", false); - String currentOrdering = defaultOrder; - //FIXME make this code decent - while (st.hasMoreTokens()) { - String token = st.nextToken(); - if (isNonPropertyToken(token)) { - if (currentOrdering != null) { - throw new DatastoreConfigurationException( - "Error while parsing sort clause: " + hqlOrderBy - + " (" + role + ")" - ); - } - currentOrdering = token; - } - else { - //Add ordering of the previous - if (currentOrdering == null) { - //default ordering - ordering.add("asc"); - } - else { - ordering.add(currentOrdering); - currentOrdering = null; - } - properties.add(token); - } - } - ordering.remove(0); //first one is the algorithm starter - // add last one ordering - if (currentOrdering == null) { - //default ordering - ordering.add(defaultOrder); - } - else { - ordering.add(currentOrdering); - currentOrdering = null; - } - int index = 0; - - for (String property : properties) { - Property p = BinderHelper.findPropertyByName(associatedClass, property); - if (p == null) { - throw new DatastoreConfigurationException( - "property from sort clause not found: " - + associatedClass.getEntityName() + "." + property - ); - } - PersistentClass pc = p.getPersistentClass(); - String table; - if (pc == null) { - table = ""; - } - - else if (pc == associatedClass - || (associatedClass instanceof SingleTableSubclass && - pc.getMappedClass().isAssignableFrom(associatedClass.getMappedClass()))) { - table = ""; - } else { - table = pc.getTable().getQuotedName() + "."; - } - - Iterator<?> propertyColumns = p.getSelectables().iterator(); - while (propertyColumns.hasNext()) { - Selectable column = (Selectable) propertyColumns.next(); - orderByBuffer.append(table) - .append(column.getText()) - .append(" ") - .append(ordering.get(index)) - .append(", "); - } - index++; - } - } - orderByString = orderByBuffer.substring(0, orderByBuffer.length() - 2); - } - return orderByString; - } - - private boolean isNonPropertyToken(String token) { - if (" ".equals(token)) return true; - if (",".equals(token)) return true; - if (token.equalsIgnoreCase("desc")) return true; - if (token.equalsIgnoreCase("asc")) return true; - return false; - } - - private Set<String> buildDiscriminatorSet(GrailsHibernatePersistentEntity domainClass) { - Set<String> theSet = new HashSet<>(); - - Mapping mapping = domainClass.getMappedForm(); - String discriminator = domainClass.getName(); - if (mapping != null && mapping.getDiscriminator() != null) { - DiscriminatorConfig discriminatorConfig = mapping.getDiscriminator(); - if(discriminatorConfig.getValue() != null) { - discriminator = discriminatorConfig.getValue(); - } - } - Mapping rootMapping = new RootMappingFetcher().getRootMapping(domainClass); - String quote = "'"; - if (rootMapping != null && rootMapping.getDatasources() != null) { - DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); - if(discriminatorConfig != null && discriminatorConfig.getType() != null && !discriminatorConfig.getType().equals("string")) - quote = ""; - } - theSet.add(quote + discriminator + quote); - - final java.util.Collection<PersistentEntity> childEntities = domainClass.getMappingContext().getDirectChildEntities(domainClass); - for (PersistentEntity subClass : childEntities) { - theSet.addAll(buildDiscriminatorSet((GrailsHibernatePersistentEntity) subClass)); - } - return theSet; - } - - private void bindCollectionWithJoinTable(HibernateToManyProperty property, - @Nonnull InFlightMetadataCollector mappings, Collection collection, PropertyConfig config, String sessionFactoryBeanName) { - - SimpleValue element; - final boolean isBasicCollectionType = property instanceof Basic; - if (isBasicCollectionType) { - element = new BasicValue(metadataBuildingContext, collection.getCollectionTable()); - } - else { - // for a normal unidirectional one-to-many we use a join column - element = new ManyToOne(metadataBuildingContext, collection.getCollectionTable()); - bindUnidirectionalOneToManyInverseValues(property, (ManyToOne) element); - } - collection.setInverse(false); - - String columnName; - - var joinColumnMappingOptional = Optional.ofNullable(config).map(PropertyConfig::getJoinTableColumnConfig); - if (isBasicCollectionType) { - final Class<?> referencedType = property.getType(); - String className = referencedType.getName(); - final boolean isEnum = referencedType.isEnum(); - if (joinColumnMappingOptional.isPresent()) { - columnName = joinColumnMappingOptional.get().getName(); - } - else { - var clazz = namingStrategy.resolveColumnName(className); - var prop = namingStrategy.resolveTableName(property.getName()); - columnName = isEnum ? clazz : new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz); - } - - if (isEnum) { - new EnumTypeBinder().bindEnumType((GrailsHibernatePersistentProperty) property, referencedType, element, columnName); - } - else { - - Mapping mapping = null; - GrailsHibernatePersistentEntity domainClass = (GrailsHibernatePersistentEntity) property.getOwner(); - if (domainClass != null) { - mapping = domainClass.getMappedForm(); - } - String typeName = property.getTypeName(); - if (typeName == null) { - Type type = mappings.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType(className); - if (type != null) { - typeName = type.getName(); - } - } - if (typeName == null) { - String domainName = property.getOwner().getName(); - throw new MappingException("Missing type or column for column["+columnName+"] on domain["+domainName+"] referencing["+className+"]"); - } - - new SimpleValueColumnBinder().bindSimpleValue(element, typeName, columnName, true); - if (joinColumnMappingOptional.isPresent()) { - Column column = new SimpleValueColumnFetcher().getColumnForSimpleValue(element); - ColumnConfig columnConfig = joinColumnMappingOptional.get(); - final PropertyConfig mappedForm = property.getMappedForm(); - new ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, columnConfig, mappedForm); - } - } - } else { - final PersistentEntity domainClass = property.getAssociatedEntity(); - - Mapping m = null; - if (domainClass != null) { - m = ((GrailsHibernatePersistentEntity) domainClass).getMappedForm(); - } - if (m != null && m.hasCompositeIdentifier()) { - CompositeIdentity ci = (CompositeIdentity) m.getIdentity(); - new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne((GrailsHibernatePersistentProperty) property, element, ci, domainClass, EMPTY_PATH); - } - else { - if (joinColumnMappingOptional.isPresent()) { - columnName = joinColumnMappingOptional.get().getName(); - } - else { - var decapitalize = domainClass.getName(); - columnName = namingStrategy.resolveColumnName(decapitalize) + FOREIGN_KEY_SUFFIX; - } - - new SimpleValueColumnBinder().bindSimpleValue(element, "long", columnName, true); - } - } - - collection.setElement(element); - - new CollectionForPropertyConfigBinder().bindCollectionForPropertyConfig(collection, config); - } - - private void bindUnidirectionalOneToManyInverseValues(HibernateToManyProperty property, ManyToOne manyToOne) { - PropertyConfig config = property.getMappedForm(); - manyToOne.setIgnoreNotFound(config.getIgnoreNotFound()); - final FetchMode fetch = config.getFetchMode(); - if(!fetch.equals(FetchMode.JOIN)) { - manyToOne.setLazy(true); - } - - final Boolean lazy = config.getLazy(); - if(lazy != null) { - manyToOne.setLazy(lazy); - } - - // set referenced entity - manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); - } - - private void bindDependentKeyValue(GrailsHibernatePersistentProperty property, DependantValue key) { - - if (LOG.isDebugEnabled()) { - LOG.debug("[CollectionBinder] binding [" + property.getName() + "] with dependant key"); - } - - PersistentEntity refDomainClass = property.getOwner(); - Mapping mapping = null; - if (refDomainClass instanceof GrailsHibernatePersistentEntity persistentEntity) { - mapping = persistentEntity.getMappedForm(); - } - boolean hasCompositeIdentifier = mapping != null && mapping.hasCompositeIdentifier(); - if (hasCompositeIdentifier && property.supportsJoinColumnMapping()) { - CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); - new CompositeIdentifierToManyToOneBinder(namingStrategy).bindCompositeIdentifierToManyToOne((GrailsHibernatePersistentProperty) property, key, ci, refDomainClass, EMPTY_PATH); - } - else { - // set type - new SimpleValueBinder(namingStrategy).bindSimpleValue(property, null, key, EMPTY_PATH); - } - } - - private DependantValue createPrimaryKeyValue(@Nonnull InFlightMetadataCollector mappings, PersistentProperty property, - Collection collection, Map<?, ?> persistentClasses) { - KeyValue keyValue; - DependantValue key; - String propertyRef = collection.getReferencedPropertyName(); - // this is to support mapping by a property - if (propertyRef == null) { - keyValue = collection.getOwner().getIdentifier(); - } else { - keyValue = (KeyValue) collection.getOwner().getProperty(propertyRef).getValue(); - } - - if (LOG.isDebugEnabled()) - LOG.debug("[CollectionBinder] creating dependant key value to table [" + keyValue.getTable().getName() + "]"); - - key = new DependantValue(metadataBuildingContext, collection.getCollectionTable(), keyValue); - - key.setTypeName(null); - // make nullable and non-updateable - key.setNullable(true); - key.setUpdateable(false); - //JPA now requires to check for sorting - key.setSorted(collection.isSorted()); - return key; - } - - private void bindUnidirectionalOneToMany(org.grails.datastore.mapping.model.types.OneToMany property, @Nonnull InFlightMetadataCollector mappings, Collection collection) { - Value v = collection.getElement(); - v.createForeignKey(); - String entityName; - if (v instanceof ManyToOne) { - ManyToOne manyToOne = (ManyToOne) v; - - entityName = manyToOne.getReferencedEntityName(); - } else { - entityName = ((OneToMany) v).getReferencedEntityName(); - } - collection.setInverse(false); - PersistentClass referenced = mappings.getEntityBinding(entityName); - Backref prop = new Backref(); - PersistentEntity owner = property.getOwner(); - prop.setEntityName(owner.getName()); - String s2 = property.getName(); - prop.setName(UNDERSCORE + new BackticksRemover().apply(owner.getJavaClass().getSimpleName()) + UNDERSCORE + new BackticksRemover().apply(s2) + "Backref"); - prop.setUpdatable(false); - prop.setInsertable(true); - prop.setCollectionRole(collection.getRole()); - prop.setValue(collection.getKey()); - prop.setOptional(true); - - referenced.addProperty(prop); - } - public Property getProperty(PersistentClass associatedClass, String propertyName) throws MappingException { try { return associatedClass.getProperty(propertyName); @@ -784,32 +208,7 @@ public class CollectionBinder { } } - private void linkBidirectionalOneToMany(Collection collection, PersistentClass associatedClass, DependantValue key, PersistentProperty otherSide) { - collection.setInverse(true); - - // Iterator mappedByColumns = associatedClass.getProperty(otherSide.getName()).getValue().getColumnIterator(); - Iterator<?> mappedByColumns = getProperty(associatedClass, otherSide.getName()).getValue().getColumns().iterator(); - while (mappedByColumns.hasNext()) { - Column column = (Column) mappedByColumns.next(); - linkValueUsingAColumnCopy(otherSide, column, key); - } - } - - private void bindManyToMany(Association property, ManyToOne element, - @Nonnull InFlightMetadataCollector mappings, String sessionFactoryBeanName) { - new ManyToOneBinder(namingStrategy).bindManyToOne(property, element, EMPTY_PATH); - element.setReferencedEntityName(property.getOwner().getName()); - } - - private void linkValueUsingAColumnCopy(PersistentProperty prop, Column column, DependantValue key) { - Column mappingColumn = new Column(); - mappingColumn.setName(column.getName()); - mappingColumn.setLength(column.getLength()); - mappingColumn.setNullable(prop.isNullable()); - mappingColumn.setSqlType(column.getSqlType()); - - mappingColumn.setValue(key); - key.addColumn(mappingColumn); - key.getTable().addColumn(mappingColumn); + public String getMultiTenantFilterCondition(PersistentEntity referenced) { + return collectionSecondPassBinder.getMultiTenantFilterCondition(referenced); } -} \ No newline at end of file +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinder.java similarity index 66% copy from grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java copy to grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinder.java index ff2995e9df..9d3268c7cf 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinder.java @@ -9,147 +9,41 @@ import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.model.types.Basic; import org.grails.datastore.mapping.model.types.ManyToMany; import org.grails.datastore.mapping.model.types.TenantId; -import org.grails.orm.hibernate.cfg.GrailsDomainBinder; -import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity; -import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty; -import org.grails.orm.hibernate.cfg.GrailsHibernateUtil; -import org.grails.orm.hibernate.cfg.HibernateToManyProperty; -import org.grails.orm.hibernate.cfg.Mapping; -import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; -import org.grails.orm.hibernate.cfg.PropertyConfig; -import org.grails.orm.hibernate.cfg.ColumnConfig; -import org.grails.orm.hibernate.cfg.DiscriminatorConfig; -import org.grails.orm.hibernate.cfg.CacheConfig; -import org.grails.orm.hibernate.cfg.JoinTable; -import org.grails.orm.hibernate.cfg.CompositeIdentity; -import org.grails.orm.hibernate.cfg.domainbinding.secondpass.GrailsCollectionSecondPass; -import org.grails.orm.hibernate.cfg.domainbinding.secondpass.ListSecondPass; -import org.grails.orm.hibernate.cfg.domainbinding.secondpass.MapSecondPass; +import org.grails.orm.hibernate.cfg.*; import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.boot.model.internal.BinderHelper; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; -import org.hibernate.mapping.Backref; -import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.*; import org.hibernate.mapping.Collection; -import org.hibernate.mapping.Column; -import org.hibernate.mapping.Component; -import org.hibernate.mapping.DependantValue; -import org.hibernate.mapping.IndexedCollection; -import org.hibernate.mapping.KeyValue; -import org.hibernate.mapping.ManyToOne; -import org.hibernate.mapping.OneToMany; -import org.hibernate.mapping.PersistentClass; -import org.hibernate.mapping.Property; -import org.hibernate.mapping.Selectable; -import org.hibernate.mapping.SimpleValue; -import org.hibernate.mapping.SingleTableSubclass; -import org.hibernate.mapping.Table; -import org.hibernate.mapping.Value; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.StringTokenizer; +import java.util.*; import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.*; /** - * Handles the binding of collections to the Hibernate runtime meta model. + * Refactored from CollectionBinder to handle collection second pass binding. */ -public class CollectionBinder { +public class CollectionSecondPassBinder { - private static final Logger LOG = LoggerFactory.getLogger(CollectionBinder.class); + private static final Logger LOG = LoggerFactory.getLogger(CollectionSecondPassBinder.class); private final MetadataBuildingContext metadataBuildingContext; - private final GrailsDomainBinder grailsDomainBinder; private final PersistentEntityNamingStrategy namingStrategy; - private final ListSecondPassBinder listSecondPassBinder; - public CollectionBinder(MetadataBuildingContext metadataBuildingContext, GrailsDomainBinder grailsDomainBinder, PersistentEntityNamingStrategy namingStrategy) { + public CollectionSecondPassBinder(MetadataBuildingContext metadataBuildingContext, PersistentEntityNamingStrategy namingStrategy) { this.metadataBuildingContext = metadataBuildingContext; - this.grailsDomainBinder = grailsDomainBinder; this.namingStrategy = namingStrategy; - this.listSecondPassBinder = new ListSecondPassBinder(metadataBuildingContext, this); - } - - /** - * First pass to bind collection to Hibernate metamodel, sets up second pass - * - * @param property The GrailsDomainClassProperty instance - * @param collection The collection - * @param owner The owning persistent class - * @param mappings The Hibernate mappings instance - * @param path The property path - */ - public void bindCollection(HibernateToManyProperty property, Collection collection, - PersistentClass owner, @Nonnull InFlightMetadataCollector mappings, String path, String sessionFactoryBeanName) { - - // set role - String propertyName = getNameForPropertyAndPath(property, path); - collection.setRole(GrailsHibernateUtil.qualify(property.getOwner().getName(), propertyName)); - - PropertyConfig pc = property.getMappedForm(); - // configure eager fetching - final FetchMode fetchMode = pc.getFetchMode(); - if (fetchMode == FetchMode.JOIN) { - collection.setFetchMode(FetchMode.JOIN); - } - else if (pc.getFetchMode() != null) { - collection.setFetchMode(pc.getFetchMode()); - } - else { - collection.setFetchMode(FetchMode.DEFAULT); - } - - if (pc.getCascade() != null) { - collection.setOrphanDelete(pc.getCascade().equals(CascadeBehavior.ALL_DELETE_ORPHAN.getValue())); - } - // if it's a one-to-many mapping - if (property.shouldBindWithForeignKey()) { - OneToMany oneToMany = new OneToMany(metadataBuildingContext, collection.getOwner()); - collection.setElement(oneToMany); - bindOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, oneToMany, mappings); - } else { - bindCollectionTable(property, mappings, collection, owner.getTable()); - - if (!property.isOwningSide()) { - collection.setInverse(true); - } - } - - if (pc.getBatchSize() != null) { - collection.setBatchSize(pc.getBatchSize()); - } - - // set up second pass - if (collection instanceof org.hibernate.mapping.Set) { - mappings.addSecondPass(new GrailsCollectionSecondPass(grailsDomainBinder, property, mappings, collection, sessionFactoryBeanName)); - } - else if (collection instanceof org.hibernate.mapping.List) { - mappings.addSecondPass(new ListSecondPass(grailsDomainBinder, property, mappings, collection, sessionFactoryBeanName)); - } - else if (collection instanceof org.hibernate.mapping.Map) { - mappings.addSecondPass(new MapSecondPass(grailsDomainBinder, property, mappings, collection, sessionFactoryBeanName)); - } - else { // Collection -> Bag - mappings.addSecondPass(new GrailsCollectionSecondPass(grailsDomainBinder, property, mappings, collection, sessionFactoryBeanName)); - } } public void bindCollectionSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, - Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { + java.util.Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { PersistentClass associatedClass = null; if (LOG.isDebugEnabled()) @@ -203,7 +97,7 @@ public class CollectionBinder { } } //NOTE: this will build the set for the in clause if it has sublcasses - Set<String> discSet = buildDiscriminatorSet((GrailsHibernatePersistentEntity) referenced); + java.util.Set<String> discSet = buildDiscriminatorSet((GrailsHibernatePersistentEntity) referenced); String inclause = String.join(",", discSet); collection.setWhere(discriminatorColumnName + " in (" + inclause + ")"); @@ -255,9 +149,9 @@ public class CollectionBinder { linkBidirectionalOneToMany(collection, associatedClass, key, otherSide); - } else if ((otherSide instanceof ManyToMany) || Map.class.isAssignableFrom(property.getType())) { + } else if ((otherSide instanceof ManyToMany) || java.util.Map.class.isAssignableFrom(property.getType())) { - bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key); + bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key, mappings, sessionFactoryBeanName); } @@ -271,7 +165,7 @@ public class CollectionBinder { } else { - bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key); + bindDependentKeyValue((GrailsHibernatePersistentProperty) property, key, mappings, sessionFactoryBeanName); } @@ -292,7 +186,7 @@ public class CollectionBinder { if (property.isBidirectional()) { if (LOG.isDebugEnabled()) - LOG.debug("[CollectionBinder] Mapping other side " + otherSide.getOwner().getName() + "." + otherSide.getName() + " -> " + collection.getCollectionTable().getName() + " as ManyToOne"); + LOG.debug("[CollectionSecondPassBinder] Mapping other side " + otherSide.getOwner().getName() + "." + otherSide.getName() + " -> " + collection.getCollectionTable().getName() + " as ManyToOne"); ManyToOne element = new ManyToOne(metadataBuildingContext, collection.getCollectionTable()); bindManyToMany((Association)otherSide, element, mappings, sessionFactoryBeanName); collection.setElement(element); @@ -314,279 +208,31 @@ public class CollectionBinder { } } - public void bindListSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, - Map<?, ?> persistentClasses, org.hibernate.mapping.List list, String sessionFactoryBeanName) { - listSecondPassBinder.bindListSecondPass(property, mappings, persistentClasses, list, sessionFactoryBeanName); - } - - public void bindMapSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, - Map<?, ?> persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { - bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); - - SimpleValue value = new BasicValue(metadataBuildingContext, map.getCollectionTable()); - - String type = ((GrailsHibernatePersistentProperty) property).getIndexColumnType("string"); - String columnName1 = getIndexColumnName(property); - new SimpleValueColumnBinder().bindSimpleValue(value, type, columnName1, true); - PropertyConfig mappedForm = property.getMappedForm(); - if (mappedForm.getIndexColumn() != null) { - Column column = new SimpleValueColumnFetcher().getColumnForSimpleValue(value); - ColumnConfig columnConfig = getSingleColumnConfig(mappedForm.getIndexColumn()); - new ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, columnConfig, mappedForm); - } - - if (!value.isTypeSpecified()) { - throw new MappingException("map index element must specify a type: " + map.getRole()); - } - map.setIndex(value); - - if(!(property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !(property instanceof ManyToMany)) { - - SimpleValue elt = new BasicValue(metadataBuildingContext, map.getCollectionTable()); - map.setElement(elt); - - Mapping mapping = null; - GrailsHibernatePersistentEntity domainClass = (GrailsHibernatePersistentEntity) property.getOwner(); - if (domainClass != null) { - mapping = domainClass.getMappedForm(); - } - String typeName = property.getTypeName(); - if (typeName == null ) { - - if(property instanceof Basic) { - Basic basic = (Basic) property; - typeName = basic.getComponentType().getName(); - } - } - if(typeName == null || typeName.equals(Object.class.getName())) { - typeName = StandardBasicTypes.STRING.getName(); - } - String columnName = getMapElementName(property, sessionFactoryBeanName); - new SimpleValueColumnBinder().bindSimpleValue(elt, typeName, columnName, false); - - elt.setTypeName(typeName); - } - - map.setInverse(false); - } - - private ColumnConfig getSingleColumnConfig(PropertyConfig propertyConfig) { - if (propertyConfig != null) { - List<ColumnConfig> columns = propertyConfig.getColumns(); - if (columns != null && !columns.isEmpty()) { - return columns.get(0); - } - } - return null; - } - - private PersistentClass getAssociatedClass(Map<?, ?> persistentClasses, HibernateToManyProperty property) { - String associatedClassName = property.getAssociatedEntity().getName(); - return (PersistentClass) persistentClasses.get(associatedClassName); - } - - private String getNameForPropertyAndPath(PersistentProperty property, String path) { - if (GrailsHibernateUtil.isNotEmpty(path)) { - return GrailsHibernateUtil.qualify(path, property.getName()); - } - return property.getName(); - } - - private void bindOneToMany(org.grails.datastore.mapping.model.types.OneToMany currentGrailsProp, OneToMany one, @Nonnull InFlightMetadataCollector mappings) { - one.setReferencedEntityName(currentGrailsProp.getAssociatedEntity().getName()); - one.setIgnoreNotFound(true); - } - - private void bindCollectionTable(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, - Collection collection, Table ownerTable) { - - String owningTableSchema = ownerTable.getSchema(); - PropertyConfig config = property.getMappedForm(); - JoinTable jt = config.getJoinTable(); - - String s = new TableForManyCalculator(namingStrategy).calculateTableForMany(property); - String tableName = (jt != null && jt.getName() != null ? jt.getName() : namingStrategy.resolveTableName(s)); - - String schemaName = new NamespaceNameExtractor().getSchemaName(mappings); - String catalogName = new NamespaceNameExtractor().getCatalogName(mappings); - if(jt != null) { - if(jt.getSchema() != null) { - schemaName = jt.getSchema(); - } - if(jt.getCatalog() != null) { - catalogName = jt.getCatalog(); - } - } - - if(schemaName == null && owningTableSchema != null) { - schemaName = owningTableSchema; - } - - collection.setCollectionTable(mappings.addTable( - schemaName, catalogName, - tableName, null, false, metadataBuildingContext)); - } - - public String getIndexColumnName(PersistentProperty property) { - PropertyConfig pc = property instanceof GrailsHibernatePersistentProperty ghpp ? ghpp.getMappedForm() : new PropertyConfig(); - if (pc.getIndexColumn() != null && pc.getIndexColumn().getColumn() != null) { - return pc.getIndexColumn().getColumn(); - } - return namingStrategy.resolveColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; - } - - private String getMapElementName(PersistentProperty property, String sessionFactoryBeanName) { - PropertyConfig pc = property instanceof GrailsHibernatePersistentProperty ghpp ? ghpp.getMappedForm() : new PropertyConfig(); - - if (hasJoinTableColumnNameMapping(pc)) { - return pc.getJoinTable().getColumn().getName(); - } - return namingStrategy.resolveColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_ELEMENT_COLUMN_NAME; - } - - private boolean hasJoinTableColumnNameMapping(PropertyConfig pc) { - return pc != null && pc.getJoinTable() != null && pc.getJoinTable().getColumn() != null && pc.getJoinTable().getColumn().getName() != null; - } - - public String getMultiTenantFilterCondition(PersistentEntity referenced) { - TenantId tenantId = referenced.getTenantId(); - if(tenantId != null) { - - String defaultColumnName = new DefaultColumnNameFetcher(namingStrategy).getDefaultColumnName(tenantId); - return ":tenantId = " + defaultColumnName; - } - else { - return null; - } - } - - @SuppressWarnings("unchecked") - private String buildOrderByClause(String hqlOrderBy, PersistentClass associatedClass, String role, String defaultOrder) { - String orderByString = null; - if (hqlOrderBy != null) { - List<String> properties = new ArrayList<>(); - List<String> ordering = new ArrayList<>(); - StringBuilder orderByBuffer = new StringBuilder(); - if (hqlOrderBy.length() == 0) { - //order by id - Iterator<?> it = associatedClass.getIdentifier().getSelectables().iterator(); - while (it.hasNext()) { - Selectable col = (Selectable) it.next(); - orderByBuffer.append(col.getText()).append(" asc").append(", "); - } - } - else { - StringTokenizer st = new StringTokenizer(hqlOrderBy, " ,", false); - String currentOrdering = defaultOrder; - //FIXME make this code decent - while (st.hasMoreTokens()) { - String token = st.nextToken(); - if (isNonPropertyToken(token)) { - if (currentOrdering != null) { - throw new DatastoreConfigurationException( - "Error while parsing sort clause: " + hqlOrderBy - + " (" + role + ")" - ); - } - currentOrdering = token; - } - else { - //Add ordering of the previous - if (currentOrdering == null) { - //default ordering - ordering.add("asc"); - } - else { - ordering.add(currentOrdering); - currentOrdering = null; - } - properties.add(token); - } - } - ordering.remove(0); //first one is the algorithm starter - // add last one ordering - if (currentOrdering == null) { - //default ordering - ordering.add(defaultOrder); - } - else { - ordering.add(currentOrdering); - currentOrdering = null; - } - int index = 0; - - for (String property : properties) { - Property p = BinderHelper.findPropertyByName(associatedClass, property); - if (p == null) { - throw new DatastoreConfigurationException( - "property from sort clause not found: " - + associatedClass.getEntityName() + "." + property - ); - } - PersistentClass pc = p.getPersistentClass(); - String table; - if (pc == null) { - table = ""; - } - - else if (pc == associatedClass - || (associatedClass instanceof SingleTableSubclass && - pc.getMappedClass().isAssignableFrom(associatedClass.getMappedClass()))) { - table = ""; - } else { - table = pc.getTable().getQuotedName() + "."; - } - - Iterator<?> propertyColumns = p.getSelectables().iterator(); - while (propertyColumns.hasNext()) { - Selectable column = (Selectable) propertyColumns.next(); - orderByBuffer.append(table) - .append(column.getText()) - .append(" ") - .append(ordering.get(index)) - .append(", "); - } - index++; - } - } - orderByString = orderByBuffer.substring(0, orderByBuffer.length() - 2); - } - return orderByString; - } - - private boolean isNonPropertyToken(String token) { - if (" ".equals(token)) return true; - if (",".equals(token)) return true; - if (token.equalsIgnoreCase("desc")) return true; - if (token.equalsIgnoreCase("asc")) return true; - return false; - } - - private Set<String> buildDiscriminatorSet(GrailsHibernatePersistentEntity domainClass) { - Set<String> theSet = new HashSet<>(); + private void bindUnidirectionalOneToMany(org.grails.datastore.mapping.model.types.OneToMany property, @Nonnull InFlightMetadataCollector mappings, Collection collection) { + Value v = collection.getElement(); + v.createForeignKey(); + String entityName; + if (v instanceof ManyToOne) { + ManyToOne manyToOne = (ManyToOne) v; - Mapping mapping = domainClass.getMappedForm(); - String discriminator = domainClass.getName(); - if (mapping != null && mapping.getDiscriminator() != null) { - DiscriminatorConfig discriminatorConfig = mapping.getDiscriminator(); - if(discriminatorConfig.getValue() != null) { - discriminator = discriminatorConfig.getValue(); - } - } - Mapping rootMapping = new RootMappingFetcher().getRootMapping(domainClass); - String quote = "'"; - if (rootMapping != null && rootMapping.getDatasources() != null) { - DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); - if(discriminatorConfig != null && discriminatorConfig.getType() != null && !discriminatorConfig.getType().equals("string")) - quote = ""; + entityName = manyToOne.getReferencedEntityName(); + } else { + entityName = ((OneToMany) v).getReferencedEntityName(); } - theSet.add(quote + discriminator + quote); + collection.setInverse(false); + PersistentClass referenced = mappings.getEntityBinding(entityName); + Backref prop = new Backref(); + PersistentEntity owner = property.getOwner(); + prop.setEntityName(owner.getName()); + String s2 = property.getName(); + prop.setName(UNDERSCORE + new BackticksRemover().apply(owner.getJavaClass().getSimpleName()) + UNDERSCORE + new BackticksRemover().apply(s2) + "Backref"); + prop.setUpdatable(false); + prop.setInsertable(true); + prop.setCollectionRole(collection.getRole()); + prop.setValue(collection.getKey()); + prop.setOptional(true); - final java.util.Collection<PersistentEntity> childEntities = domainClass.getMappingContext().getDirectChildEntities(domainClass); - for (PersistentEntity subClass : childEntities) { - theSet.addAll(buildDiscriminatorSet((GrailsHibernatePersistentEntity) subClass)); - } - return theSet; + referenced.addProperty(prop); } private void bindCollectionWithJoinTable(HibernateToManyProperty property, @@ -696,10 +342,17 @@ public class CollectionBinder { manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); } - private void bindDependentKeyValue(GrailsHibernatePersistentProperty property, DependantValue key) { + private void bindManyToMany(Association property, ManyToOne element, + @Nonnull InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + new ManyToOneBinder(namingStrategy).bindManyToOne(property, element, EMPTY_PATH); + element.setReferencedEntityName(property.getOwner().getName()); + } + + private void bindDependentKeyValue(GrailsHibernatePersistentProperty property, DependantValue key, + @Nonnull InFlightMetadataCollector mappings, String sessionFactoryBeanName) { if (LOG.isDebugEnabled()) { - LOG.debug("[CollectionBinder] binding [" + property.getName() + "] with dependant key"); + LOG.debug("[CollectionSecondPassBinder] binding [" + property.getName() + "] with dependant key"); } PersistentEntity refDomainClass = property.getOwner(); @@ -718,8 +371,44 @@ public class CollectionBinder { } } + private void linkBidirectionalOneToMany(Collection collection, PersistentClass associatedClass, DependantValue key, PersistentProperty otherSide) { + collection.setInverse(true); + + // Iterator mappedByColumns = associatedClass.getProperty(otherSide.getName()).getValue().getColumnIterator(); + Iterator<?> mappedByColumns = getProperty(associatedClass, otherSide.getName()).getValue().getColumns().iterator(); + while (mappedByColumns.hasNext()) { + Column column = (Column) mappedByColumns.next(); + linkValueUsingAColumnCopy(otherSide, column, key); + } + } + + private void linkValueUsingAColumnCopy(PersistentProperty prop, Column column, DependantValue key) { + Column mappingColumn = new Column(); + mappingColumn.setName(column.getName()); + mappingColumn.setLength(column.getLength()); + mappingColumn.setNullable(prop.isNullable()); + mappingColumn.setSqlType(column.getSqlType()); + + mappingColumn.setValue(key); + key.addColumn(mappingColumn); + key.getTable().addColumn(mappingColumn); + } + + public Property getProperty(PersistentClass associatedClass, String propertyName) throws MappingException { + try { + return associatedClass.getProperty(propertyName); + } + catch (MappingException e) { + //maybe it's squirreled away in a composite primary key + if (associatedClass.getKey() instanceof Component) { + return ((Component) associatedClass.getKey()).getProperty(propertyName); + } + throw e; + } + } + private DependantValue createPrimaryKeyValue(@Nonnull InFlightMetadataCollector mappings, PersistentProperty property, - Collection collection, Map<?, ?> persistentClasses) { + Collection collection, java.util.Map<?, ?> persistentClasses) { KeyValue keyValue; DependantValue key; String propertyRef = collection.getReferencedPropertyName(); @@ -731,7 +420,7 @@ public class CollectionBinder { } if (LOG.isDebugEnabled()) - LOG.debug("[CollectionBinder] creating dependant key value to table [" + keyValue.getTable().getName() + "]"); + LOG.debug("[CollectionSecondPassBinder] creating dependant key value to table [" + keyValue.getTable().getName() + "]"); key = new DependantValue(metadataBuildingContext, collection.getCollectionTable(), keyValue); @@ -744,72 +433,144 @@ public class CollectionBinder { return key; } - private void bindUnidirectionalOneToMany(org.grails.datastore.mapping.model.types.OneToMany property, @Nonnull InFlightMetadataCollector mappings, Collection collection) { - Value v = collection.getElement(); - v.createForeignKey(); - String entityName; - if (v instanceof ManyToOne) { - ManyToOne manyToOne = (ManyToOne) v; + private java.util.Set<String> buildDiscriminatorSet(GrailsHibernatePersistentEntity domainClass) { + java.util.Set<String> theSet = new java.util.HashSet<>(); - entityName = manyToOne.getReferencedEntityName(); - } else { - entityName = ((OneToMany) v).getReferencedEntityName(); + Mapping mapping = domainClass.getMappedForm(); + String discriminator = domainClass.getName(); + if (mapping != null && mapping.getDiscriminator() != null) { + DiscriminatorConfig discriminatorConfig = mapping.getDiscriminator(); + if(discriminatorConfig.getValue() != null) { + discriminator = discriminatorConfig.getValue(); + } } - collection.setInverse(false); - PersistentClass referenced = mappings.getEntityBinding(entityName); - Backref prop = new Backref(); - PersistentEntity owner = property.getOwner(); - prop.setEntityName(owner.getName()); - String s2 = property.getName(); - prop.setName(UNDERSCORE + new BackticksRemover().apply(owner.getJavaClass().getSimpleName()) + UNDERSCORE + new BackticksRemover().apply(s2) + "Backref"); - prop.setUpdatable(false); - prop.setInsertable(true); - prop.setCollectionRole(collection.getRole()); - prop.setValue(collection.getKey()); - prop.setOptional(true); + Mapping rootMapping = new RootMappingFetcher().getRootMapping(domainClass); + String quote = "'"; + if (rootMapping != null && rootMapping.getDatasources() != null) { + DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); + if(discriminatorConfig != null && discriminatorConfig.getType() != null && !discriminatorConfig.getType().equals("string")) + quote = ""; + } + theSet.add(quote + discriminator + quote); - referenced.addProperty(prop); + final java.util.Collection<PersistentEntity> childEntities = domainClass.getMappingContext().getDirectChildEntities(domainClass); + for (PersistentEntity subClass : childEntities) { + theSet.addAll(buildDiscriminatorSet((GrailsHibernatePersistentEntity) subClass)); + } + return theSet; } - public Property getProperty(PersistentClass associatedClass, String propertyName) throws MappingException { - try { - return associatedClass.getProperty(propertyName); - } - catch (MappingException e) { - //maybe it's squirreled away in a composite primary key - if (associatedClass.getKey() instanceof Component) { - return ((Component) associatedClass.getKey()).getProperty(propertyName); + @SuppressWarnings("unchecked") + private String buildOrderByClause(String hqlOrderBy, PersistentClass associatedClass, String role, String defaultOrder) { + String orderByString = null; + if (hqlOrderBy != null) { + java.util.List<String> properties = new java.util.ArrayList<>(); + java.util.List<String> ordering = new java.util.ArrayList<>(); + StringBuilder orderByBuffer = new StringBuilder(); + if (hqlOrderBy.length() == 0) { + //order by id + Iterator<?> it = associatedClass.getIdentifier().getSelectables().iterator(); + while (it.hasNext()) { + Selectable col = (Selectable) it.next(); + orderByBuffer.append(col.getText()).append(" asc").append(", "); + } } - throw e; - } - } + else { + StringTokenizer st = new StringTokenizer(hqlOrderBy, " ,", false); + String currentOrdering = defaultOrder; + //FIXME make this code decent + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (isNonPropertyToken(token)) { + if (currentOrdering != null) { + throw new DatastoreConfigurationException( + "Error while parsing sort clause: " + hqlOrderBy + + " (" + role + ")" + ); + } + currentOrdering = token; + } + else { + //Add ordering of the previous + if (currentOrdering == null) { + //default ordering + ordering.add("asc"); + } + else { + ordering.add(currentOrdering); + currentOrdering = null; + } + properties.add(token); + } + } + ordering.remove(0); //first one is the algorithm starter + // add last one ordering + if (currentOrdering == null) { + //default ordering + ordering.add(defaultOrder); + } + else { + ordering.add(currentOrdering); + currentOrdering = null; + } + int index = 0; - private void linkBidirectionalOneToMany(Collection collection, PersistentClass associatedClass, DependantValue key, PersistentProperty otherSide) { - collection.setInverse(true); + for (String property : properties) { + Property p = BinderHelper.findPropertyByName(associatedClass, property); + if (p == null) { + throw new DatastoreConfigurationException( + "property from sort clause not found: " + + associatedClass.getEntityName() + "." + property + ); + } + PersistentClass pc = p.getPersistentClass(); + String table; + if (pc == null) { + table = ""; + } - // Iterator mappedByColumns = associatedClass.getProperty(otherSide.getName()).getValue().getColumnIterator(); - Iterator<?> mappedByColumns = getProperty(associatedClass, otherSide.getName()).getValue().getColumns().iterator(); - while (mappedByColumns.hasNext()) { - Column column = (Column) mappedByColumns.next(); - linkValueUsingAColumnCopy(otherSide, column, key); + else if (pc == associatedClass + || (associatedClass instanceof SingleTableSubclass && + pc.getMappedClass().isAssignableFrom(associatedClass.getMappedClass()))) { + table = ""; + } else { + table = pc.getTable().getQuotedName() + "."; + } + + Iterator<?> propertyColumns = p.getSelectables().iterator(); + while (propertyColumns.hasNext()) { + Selectable column = (Selectable) propertyColumns.next(); + orderByBuffer.append(table) + .append(column.getText()) + .append(" ") + .append(ordering.get(index)) + .append(", "); + } + index++; + } + } + orderByString = orderByBuffer.substring(0, orderByBuffer.length() - 2); } + return orderByString; } - private void bindManyToMany(Association property, ManyToOne element, - @Nonnull InFlightMetadataCollector mappings, String sessionFactoryBeanName) { - new ManyToOneBinder(namingStrategy).bindManyToOne(property, element, EMPTY_PATH); - element.setReferencedEntityName(property.getOwner().getName()); + private boolean isNonPropertyToken(String token) { + if (" ".equals(token)) return true; + if (",".equals(token)) return true; + if (token.equalsIgnoreCase("desc")) return true; + if (token.equalsIgnoreCase("asc")) return true; + return false; } - private void linkValueUsingAColumnCopy(PersistentProperty prop, Column column, DependantValue key) { - Column mappingColumn = new Column(); - mappingColumn.setName(column.getName()); - mappingColumn.setLength(column.getLength()); - mappingColumn.setNullable(prop.isNullable()); - mappingColumn.setSqlType(column.getSqlType()); + public String getMultiTenantFilterCondition(PersistentEntity referenced) { + TenantId tenantId = referenced.getTenantId(); + if(tenantId != null) { - mappingColumn.setValue(key); - key.addColumn(mappingColumn); - key.getTable().addColumn(mappingColumn); + String defaultColumnName = new DefaultColumnNameFetcher(namingStrategy).getDefaultColumnName(tenantId); + return ":tenantId = " + defaultColumnName; + } + else { + return null; + } } -} \ No newline at end of file +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinder.java new file mode 100644 index 0000000000..bf240de17a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinder.java @@ -0,0 +1,114 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import jakarta.annotation.Nonnull; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Basic; +import org.grails.orm.hibernate.cfg.ColumnConfig; +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity; +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.HibernateToManyProperty; +import org.grails.orm.hibernate.cfg.Mapping; +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; +import org.grails.orm.hibernate.cfg.PropertyConfig; +import org.hibernate.MappingException; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.IndexedCollection; +import org.hibernate.mapping.SimpleValue; +import org.hibernate.type.StandardBasicTypes; + +import java.util.List; +import java.util.Map; + +import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.UNDERSCORE; + +/** + * Refactored from CollectionBinder to handle map second pass binding. + */ +public class MapSecondPassBinder { + private final MetadataBuildingContext metadataBuildingContext; + private final PersistentEntityNamingStrategy namingStrategy; + private final CollectionBinder collectionBinder; + + public MapSecondPassBinder(MetadataBuildingContext metadataBuildingContext, PersistentEntityNamingStrategy namingStrategy, CollectionBinder collectionBinder) { + this.metadataBuildingContext = metadataBuildingContext; + this.namingStrategy = namingStrategy; + this.collectionBinder = collectionBinder; + } + + public void bindMapSecondPass(HibernateToManyProperty property, @Nonnull InFlightMetadataCollector mappings, + Map<?, ?> persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { + collectionBinder.bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); + + SimpleValue value = new BasicValue(metadataBuildingContext, map.getCollectionTable()); + + String type = ((GrailsHibernatePersistentProperty) property).getIndexColumnType("string"); + String columnName1 = collectionBinder.getIndexColumnName(property); + new SimpleValueColumnBinder().bindSimpleValue(value, type, columnName1, true); + PropertyConfig mappedForm = property.getMappedForm(); + if (mappedForm.getIndexColumn() != null) { + Column column = new SimpleValueColumnFetcher().getColumnForSimpleValue(value); + ColumnConfig columnConfig = getSingleColumnConfig(mappedForm.getIndexColumn()); + new ColumnConfigToColumnBinder().bindColumnConfigToColumn(column, columnConfig, mappedForm); + } + + if (!value.isTypeSpecified()) { + throw new MappingException("map index element must specify a type: " + map.getRole()); + } + map.setIndex(value); + + if(!(property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !(property instanceof org.grails.datastore.mapping.model.types.ManyToMany)) { + + SimpleValue elt = new BasicValue(metadataBuildingContext, map.getCollectionTable()); + map.setElement(elt); + + Mapping mapping = null; + GrailsHibernatePersistentEntity domainClass = (GrailsHibernatePersistentEntity) property.getOwner(); + if (domainClass != null) { + mapping = domainClass.getMappedForm(); + } + String typeName = property.getTypeName(); + if (typeName == null ) { + + if(property instanceof Basic) { + Basic basic = (Basic) property; + typeName = basic.getComponentType().getName(); + } + } + if(typeName == null || typeName.equals(Object.class.getName())) { + typeName = StandardBasicTypes.STRING.getName(); + } + String columnName = getMapElementName(property, sessionFactoryBeanName); + new SimpleValueColumnBinder().bindSimpleValue(elt, typeName, columnName, false); + + elt.setTypeName(typeName); + } + + map.setInverse(false); + } + + private ColumnConfig getSingleColumnConfig(PropertyConfig propertyConfig) { + if (propertyConfig != null) { + List<ColumnConfig> columns = propertyConfig.getColumns(); + if (columns != null && !columns.isEmpty()) { + return columns.get(0); + } + } + return null; + } + + private String getMapElementName(PersistentProperty property, String sessionFactoryBeanName) { + PropertyConfig pc = property instanceof GrailsHibernatePersistentProperty ghpp ? ghpp.getMappedForm() : new PropertyConfig(); + + if (hasJoinTableColumnNameMapping(pc)) { + return pc.getJoinTable().getColumn().getName(); + } + return namingStrategy.resolveColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_ELEMENT_COLUMN_NAME; + } + + private boolean hasJoinTableColumnNameMapping(PropertyConfig pc) { + return pc != null && pc.getJoinTable() != null && pc.getJoinTable().getColumn() != null && pc.getJoinTable().getColumn().getName() != null; + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinderSpec.groovy new file mode 100644 index 0000000000..c0bce6e197 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionSecondPassBinderSpec.groovy @@ -0,0 +1,81 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity +import org.grails.orm.hibernate.cfg.HibernateToManyProperty +import org.hibernate.mapping.Collection +import org.hibernate.mapping.RootClass + +class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { + + void setupSpec() { + manager.addAllDomainClasses([ + Author, + Book + ]) + } + + void "Test bind collection second pass"() { + given: + def collector = getCollector() + def binder = getGrailsDomainBinder() + def collectionBinder = binder.getCollectionBinder() + def collectionSecondPassBinder = new CollectionSecondPassBinder(binder.getMetadataBuildingContext(), binder.getNamingStrategy()) + + def authorEntity = getPersistentEntity(Author) as GrailsHibernatePersistentEntity + def bookEntity = getPersistentEntity(Book) as GrailsHibernatePersistentEntity + + // Register referenced entity in Hibernate + binder.bindRoot(bookEntity, collector, "sessionFactory") + + // Manually create RootClass for the main entity + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(authorEntity.name) + rootClass.setJpaEntityName(authorEntity.name) + rootClass.setTable(collector.addTable(null, null, "AUTHOR", null, false, binder.getMetadataBuildingContext())) + + // Add a primary key to avoid NPE + def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) + def idCol = new org.hibernate.mapping.Column("id") + rootClass.table.addColumn(idCol) + pk.addColumn(idCol) + rootClass.table.setPrimaryKey(pk) + collector.addEntityBinding(rootClass) + + def booksProp = authorEntity.getPropertyByName("books") as HibernateToManyProperty + def set = new org.hibernate.mapping.Set(binder.getMetadataBuildingContext(), rootClass) + set.setRole(authorEntity.name + ".books") + + // Initial first pass binding + collectionBinder.bindCollection(booksProp, set, rootClass, collector, "", "sessionFactory") + + // Prepare persistentClasses map + Map persistentClasses = [ + (authorEntity.name): rootClass, + (bookEntity.name): collector.getEntityBinding(bookEntity.name) + ] + + when: + collectionSecondPassBinder.bindCollectionSecondPass(booksProp, collector, persistentClasses, set, "sessionFactory") + collector.processSecondPasses(binder.getMetadataBuildingContext()) + + then: + set.key != null + set.element != null + set.collectionTable != null + } +} + +@Entity +class Author { + Long id + Set<Book> books + static hasMany = [books: Book] +} + +@Entity +class Book { + Long id + String title +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinderSpec.groovy new file mode 100644 index 0000000000..0999fa20fe --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/MapSecondPassBinderSpec.groovy @@ -0,0 +1,83 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import org.grails.orm.hibernate.cfg.GrailsHibernatePersistentEntity +import org.grails.orm.hibernate.cfg.HibernateToManyProperty +import org.hibernate.mapping.Map +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.SimpleValue + +class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { + + void setupSpec() { + manager.addAllDomainClasses([ + MapAuthorBinder, + MapBookBinder + ]) + } + + void "Test bind map second pass"() { + given: + def collector = getCollector() + def binder = getGrailsDomainBinder() + def collectionBinder = binder.getCollectionBinder() + def mapSecondPassBinder = new MapSecondPassBinder(binder.getMetadataBuildingContext(), binder.getNamingStrategy(), collectionBinder) + + def authorEntity = getPersistentEntity(MapAuthorBinder) as GrailsHibernatePersistentEntity + def bookEntity = getPersistentEntity(MapBookBinder) as GrailsHibernatePersistentEntity + + // Register referenced entity in Hibernate + binder.bindRoot(bookEntity, collector, "sessionFactory") + + // Manually create RootClass for the main entity + def rootClass = new RootClass(binder.getMetadataBuildingContext()) + rootClass.setEntityName(authorEntity.name) + rootClass.setJpaEntityName(authorEntity.name) + rootClass.setTable(collector.addTable(null, null, "MAP_AUTHOR_BINDER", null, false, binder.getMetadataBuildingContext())) + + // Add a primary key to avoid NPE + def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) + def idCol = new org.hibernate.mapping.Column("id") + rootClass.table.addColumn(idCol) + pk.addColumn(idCol) + rootClass.table.setPrimaryKey(pk) + collector.addEntityBinding(rootClass) + + def booksProp = authorEntity.getPropertyByName("books") as HibernateToManyProperty + def map = new org.hibernate.mapping.Map(binder.getMetadataBuildingContext(), rootClass) + map.setRole(authorEntity.name + ".books") + + // Initial first pass binding + collectionBinder.bindCollection(booksProp, map, rootClass, collector, "", "sessionFactory") + + // Prepare persistentClasses map + java.util.Map persistentClasses = [ + (authorEntity.name): rootClass, + (bookEntity.name): collector.getEntityBinding(bookEntity.name) + ] + + when: + mapSecondPassBinder.bindMapSecondPass(booksProp, collector, persistentClasses, map, "sessionFactory") + collector.processSecondPasses(binder.getMetadataBuildingContext()) + + then: + map.index != null + map.index instanceof SimpleValue + ((SimpleValue)map.index).typeName == "string" + map.element != null + } +} + +@Entity +class MapAuthorBinder { + Long id + java.util.Map<String, MapBookBinder> books + static hasMany = [books: MapBookBinder] +} + +@Entity +class MapBookBinder { + Long id + String title +}
