You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/04/06 19:16:55 UTC

(grails-core) 05/12: H7: expand test coverage for ManyToMany properties and List second pass binding

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

borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit ad10d58a6cd1cbc966e4202410b528d336e54a6d
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Apr 4 22:48:12 2026 -0500

    H7: expand test coverage for ManyToMany properties and List second pass binding
---
 .../domainbinding/secondpass/ListSecondPass.java   |   2 +-
 .../secondpass/ListSecondPassBinder.java           |  90 ++--
 .../gorm/specs/HibernateGormDatastoreSpec.groovy   |   4 +-
 .../HibernateManyToManyPropertySpec.groovy         |  59 +++
 .../secondpass/ListSecondPassBinderSpec.groovy     | 454 +++++++++++----------
 5 files changed, 353 insertions(+), 256 deletions(-)

diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java
index ee6381b30e..3d5e6c832e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPass.java
@@ -41,7 +41,7 @@ public class ListSecondPass implements org.hibernate.boot.spi.SecondPass, Grails
 
     @Override
     public void doSecondPass(Map persistentClasses) throws MappingException {
-        listSecondPassBinder.bindListSecondPass(property, persistentClasses);
+        listSecondPassBinder.bindListSecondPass(property);
         createCollectionKeys(property.getCollection());
     }
 }
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinder.java
index f4006cb0b0..8ec6a0c493 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinder.java
@@ -18,8 +18,6 @@
  */
 package org.grails.orm.hibernate.cfg.domainbinding.secondpass;
 
-import java.util.Map;
-
 import jakarta.annotation.Nonnull;
 
 import org.hibernate.MappingException;
@@ -39,7 +37,6 @@ import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
 import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateAssociation;
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToManyProperty;
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToOneProperty;
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty;
 import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover;
 
@@ -68,7 +65,7 @@ public class ListSecondPassBinder {
         this.mappings = mappings;
     }
 
-    public void bindListSecondPass(@Nonnull HibernateToManyProperty property, Map<?, ?> persistentClasses) {
+    public void bindListSecondPass(@Nonnull HibernateToManyProperty property) {
 
         List list = (List) property.getCollection();
         collectionSecondPassBinder.bindCollectionSecondPass(property);
@@ -93,57 +90,54 @@ public class ListSecondPassBinder {
 
         if (property.isBidirectional()) {
 
-            String entityName;
             HibernateAssociation inverseSide = property.getHibernateInverseSide();
-            if (inverseSide instanceof HibernateManyToOneProperty manyToOne) {
-                entityName = manyToOne.getReferencedEntityName();
-            } else {
-                entityName = inverseSide.getReferencedEntityName();
-            }
+            String entityName = inverseSide.getHibernateOwner().getName();
 
             PersistentClass referenced = mappings.getEntityBinding(entityName);
 
-            boolean compositeIdProperty = property.getHibernateInverseSide().isCompositeIdProperty();
-            if (!compositeIdProperty) {
-                Backref prop = new Backref();
-                final 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.setSelectable(false);
-                prop.setUpdatable(false);
-                if (isManyToMany) {
-                    prop.setInsertable(false);
-                }
-                prop.setCollectionRole(list.getRole());
-                prop.setValue(list.getKey());
-
-                DependantValue value = (DependantValue) prop.getValue();
-                if (!property.isCircular()) {
-                    value.setNullable(false);
+            if (referenced != null) {
+                boolean compositeIdProperty = property.getHibernateInverseSide().isCompositeIdProperty();
+                if (!compositeIdProperty) {
+                    Backref prop = new Backref();
+                    final 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.setSelectable(false);
+                    prop.setUpdatable(false);
+                    if (isManyToMany) {
+                        prop.setInsertable(false);
+                    }
+                    prop.setCollectionRole(list.getRole());
+                    prop.setValue(list.getKey());
+
+                    DependantValue value = (DependantValue) prop.getValue();
+                    if (!property.isCircular()) {
+                        value.setNullable(false);
+                    }
+                    value.setUpdateable(true);
+                    prop.setOptional(false);
+
+                    referenced.addProperty(prop);
                 }
-                value.setUpdateable(true);
-                prop.setOptional(false);
-
-                referenced.addProperty(prop);
-            }
 
-            if ((!list.getKey().isNullable() && !list.isInverse()) || compositeIdProperty) {
-                IndexBackref ib = new IndexBackref();
-                ib.setName(UNDERSCORE + property.getName() + "IndexBackref");
-                ib.setUpdatable(false);
-                ib.setSelectable(false);
-                if (isManyToMany) {
-                    ib.setInsertable(false);
+                if ((!list.getKey().isNullable() && !list.isInverse()) || compositeIdProperty) {
+                    IndexBackref ib = new IndexBackref();
+                    ib.setName(UNDERSCORE + property.getName() + "IndexBackref");
+                    ib.setUpdatable(false);
+                    ib.setSelectable(false);
+                    if (isManyToMany) {
+                        ib.setInsertable(false);
+                    }
+                    ib.setCollectionRole(list.getRole());
+                    ib.setEntityName(list.getOwner().getEntityName());
+                    ib.setValue(list.getIndex());
+                    referenced.addProperty(ib);
                 }
-                ib.setCollectionRole(list.getRole());
-                ib.setEntityName(list.getOwner().getEntityName());
-                ib.setValue(list.getIndex());
-                referenced.addProperty(ib);
             }
         }
     }
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
index 29209d2c82..1ca4db98c0 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
@@ -165,8 +165,8 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T
         datastore.connect() as HibernateSession
     }
 
-    protected PersistentEntity getPersistentEntity(Class clazz) {
-        getMappingContext().getPersistentEntity(clazz.typeName)
+    protected GrailsHibernatePersistentEntity getPersistentEntity(Class clazz) {
+        getMappingContext().getPersistentEntity(clazz.typeName) as GrailsHibernatePersistentEntity
     }
 
     protected HibernateQuery getQuery(Class clazz) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateManyToManyPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateManyToManyPropertySpec.groovy
new file mode 100644
index 0000000000..e62ab1ea88
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateManyToManyPropertySpec.groovy
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate.cfg.domainbinding.hibernate
+
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.persistence.Entity
+
+class HibernateManyToManyPropertySpec extends HibernateGormDatastoreSpec {
+
+    void setupSpec() {
+        manager.addAllDomainClasses([HMMPA, HMMPB])
+    }
+
+    def "test HibernateManyToManyProperty basic methods"() {
+        given:
+        def entityA = (HibernatePersistentEntity) getMappingContext().getPersistentEntity(HMMPA.name)
+        def property = (HibernateManyToManyProperty) entityA.getPropertyByName("others")
+
+        expect:
+        property.getHibernateAssociatedEntity().name == HMMPB.name
+        property.getReferencedEntityName() == HMMPB.name
+        property.isManyToMany()
+        !property.isOneToMany()
+        property.isLazy()
+        !property.isAssociationColumnNullable()
+    }
+}
+
+@Entity
+class HMMPA {
+    Long id
+    static hasMany = [others: HMMPB]
+    static mapping = {
+        others joinTable: [name: "h_m_m_p_a_others"]
+    }
+}
+
+@Entity
+class HMMPB {
+    Long id
+    static hasMany = [owners: HMMPA]
+    static belongsTo = [owners: HMMPA]
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy
index 5d5381976d..291b53dc7e 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy
@@ -20,259 +20,303 @@
 package org.grails.orm.hibernate.cfg.domainbinding.secondpass
 
 import grails.gorm.specs.HibernateGormDatastoreSpec
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty
-
-import org.hibernate.mapping.OneToMany
-import org.hibernate.mapping.RootClass
-import org.hibernate.boot.spi.MetadataBuildingContext
+import grails.persistence.Entity
+import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
-import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ForeignKeyOneToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneValuesBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher
-import org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.OneToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentUpdater
-import org.grails.orm.hibernate.cfg.domainbinding.util.BasicValueCreator
-import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsPropertyBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.IdentityBinder
-import org.hibernate.boot.spi.InFlightMetadataCollector
-import org.grails.orm.hibernate.cfg.domainbinding.binder.VersionBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleIdBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.PropertyBinder
-import org.hibernate.mapping.BasicValue
-import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover
-import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher
-import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher
+import org.grails.orm.hibernate.cfg.domainbinding.binder.*
 import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder
-import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator
-import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SubClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SubclassMappingBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.RootBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.RootPersistentClassCommonValuesBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.DiscriminatorPropertyBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ColumnConfigToColumnBinder
-
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassPropertiesBinder
-import org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.JoinedSubClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.UnionSubclassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubclassBinder
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.*
+import org.grails.orm.hibernate.cfg.domainbinding.util.*
+import org.hibernate.MappingException
+import org.hibernate.boot.spi.InFlightMetadataCollector
+import org.hibernate.boot.spi.MetadataBuildingContext
+import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
+import org.hibernate.mapping.*
 
 class ListSecondPassBinderSpec extends HibernateGormDatastoreSpec {
 
-    protected Map getBinders(GrailsDomainBinder binder, InFlightMetadataCollector collector = getCollector()) {
-        MetadataBuildingContext metadataBuildingContext = binder.getMetadataBuildingContext()
-        PersistentEntityNamingStrategy namingStrategy = binder.getNamingStrategy()
-        JdbcEnvironment jdbcEnvironment = binder.getJdbcEnvironment()
-        BackticksRemover backticksRemover = new BackticksRemover()
-        DefaultColumnNameFetcher defaultColumnNameFetcher = new DefaultColumnNameFetcher(namingStrategy, backticksRemover)
-        ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher = new ColumnNameForPropertyAndPathFetcher(namingStrategy, defaultColumnNameFetcher, backticksRemover)
-        CollectionHolder collectionHolder = new CollectionHolder(metadataBuildingContext)
-        SimpleValueBinder simpleValueBinder = new SimpleValueBinder(metadataBuildingContext, namingStrategy, jdbcEnvironment)
-        EnumTypeBinder enumTypeBinderToUse = new EnumTypeBinder(metadataBuildingContext, columnNameForPropertyAndPathFetcher,namingStrategy)
-        SimpleValueColumnFetcher simpleValueColumnFetcher = new SimpleValueColumnFetcher()
-        CompositeIdentifierToManyToOneBinder compositeIdentifierToManyToOneBinder = new CompositeIdentifierToManyToOneBinder(
-
-                new org.grails.orm.hibernate.cfg.domainbinding.util.ForeignKeyColumnCountCalculator(),
-                namingStrategy,
-                defaultColumnNameFetcher,
-                backticksRemover,
-                simpleValueBinder
-        )
-        OneToOneBinder oneToOneBinder = new OneToOneBinder(metadataBuildingContext, simpleValueBinder)
-        ManyToOneBinder manyToOneBinder = new ManyToOneBinder(metadataBuildingContext, namingStrategy, simpleValueBinder, new ManyToOneValuesBinder(), compositeIdentifierToManyToOneBinder)
-        ForeignKeyOneToOneBinder foreignKeyOneToOneBinder = new ForeignKeyOneToOneBinder(manyToOneBinder, simpleValueColumnFetcher)
-
-        CollectionBinder collectionBinder = new CollectionBinder(
-                metadataBuildingContext,
-                namingStrategy
-                ,
-                simpleValueBinder,
-                enumTypeBinderToUse,
-                manyToOneBinder,
-                compositeIdentifierToManyToOneBinder,
-                simpleValueColumnFetcher
-                ,
-                collectionHolder,
-                collector
-        )
-        PropertyFromValueCreator propertyFromValueCreator = new PropertyFromValueCreator()
-        ComponentUpdater componentUpdater = new ComponentUpdater(propertyFromValueCreator)
-        ComponentBinder componentBinder = new ComponentBinder(
-                metadataBuildingContext,
-                binder.getMappingCacheHolder(),
-                componentUpdater
-        )
-
-        GrailsPropertyBinder propertyBinder = new GrailsPropertyBinder(
-
-
-                enumTypeBinderToUse,
-                componentBinder,
-                collectionBinder,
-                simpleValueBinder
-                ,
-                oneToOneBinder,
-                manyToOneBinder,
-                foreignKeyOneToOneBinder
-
-        )
-        CompositeIdBinder compositeIdBinder = new CompositeIdBinder(metadataBuildingContext, componentUpdater, propertyBinder)
-        PropertyBinder propertyBinderHelper = new PropertyBinder()
-        SimpleIdBinder simpleIdBinder = new SimpleIdBinder(metadataBuildingContext, new BasicValueCreator(metadataBuildingContext, jdbcEnvironment, namingStrategy), simpleValueBinder, propertyBinderHelper)
-        IdentityBinder identityBinder = new IdentityBinder(simpleIdBinder, compositeIdBinder)
-        VersionBinder versionBinder = new VersionBinder(metadataBuildingContext, simpleValueBinder, propertyBinderHelper, BasicValue::new)
-
-        ClassBinder classBinder = new ClassBinder(collector)
-        ClassPropertiesBinder classPropertiesBinder = new ClassPropertiesBinder(propertyBinder, propertyFromValueCreator)
-        MultiTenantFilterBinder multiTenantFilterBinder = new MultiTenantFilterBinder(new org.grails.orm.hibernate.cfg.domainbinding.util.GrailsPropertyResolver(), new org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterDefinitionBinder(), collector, defaultColumnNameFetcher)
-        JoinedSubClassBinder joinedSubClassBinder = new JoinedSubClassBinder(metadataBuildingContext, namingStrategy, new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), columnNameForPropertyAndPathFetcher, classBinder, collector)
-        UnionSubclassBinder unionSubclassBinder = new UnionSubclassBinder(metadataBuildingContext, namingStrategy, classBinder, collector)
-        SingleTableSubclassBinder singleTableSubclassBinder = new SingleTableSubclassBinder(classBinder, metadataBuildingContext)
-
-        SubclassMappingBinder subclassMappingBinder = new SubclassMappingBinder(joinedSubClassBinder, unionSubclassBinder, singleTableSubclassBinder, classPropertiesBinder)
-        SubClassBinder subClassBinder = new SubClassBinder(subclassMappingBinder, multiTenantFilterBinder, "dataSource")
-        RootPersistentClassCommonValuesBinder rootPersistentClassCommonValuesBinder = new RootPersistentClassCommonValuesBinder(metadataBuildingContext, namingStrategy, identityBinder, versionBinder, classBinder, classPropertiesBinder, collector)
-        DiscriminatorPropertyBinder discriminatorPropertyBinder = new DiscriminatorPropertyBinder(metadataBuildingContext, binder.getMappingCacheHolder(), new org.grails.orm.hibernate.cfg.domainbinding.binder.ConfiguredDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), new ColumnConfigToColumnBinder()), new org.grails.orm.hibernate.cfg.domainbinding.binder.DefaultDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.Si [...]
-        RootBinder rootBinder = new RootBinder("default", multiTenantFilterBinder, subClassBinder, rootPersistentClassCommonValuesBinder, discriminatorPropertyBinder, collector, binder.getMappingCacheHolder())
+    protected java.util.Map getBinders(GrailsDomainBinder binder, InFlightMetadataCollector collector = getCollector()) {
+        MetadataBuildingContext mbc = binder.getMetadataBuildingContext()
+        PersistentEntityNamingStrategy ns = binder.getNamingStrategy()
+        JdbcEnvironment je = binder.getJdbcEnvironment()
+        BackticksRemover br = new BackticksRemover()
+        DefaultColumnNameFetcher dcnf = new DefaultColumnNameFetcher(ns, br)
+        ColumnNameForPropertyAndPathFetcher cnfpapf = new ColumnNameForPropertyAndPathFetcher(ns, dcnf, br)
+        CollectionHolder ch = new CollectionHolder(mbc)
+        SimpleValueBinder svb = new SimpleValueBinder(mbc, ns, je)
+        EnumTypeBinder etb = new EnumTypeBinder(mbc, cnfpapf, ns)
+        SimpleValueColumnFetcher svcf = new SimpleValueColumnFetcher()
+        CompositeIdentifierToManyToOneBinder citmto = new CompositeIdentifierToManyToOneBinder(
+                new ForeignKeyColumnCountCalculator(), ns, dcnf, br, svb)
+        OneToOneBinder otob = new OneToOneBinder(mbc, svb)
+        ManyToOneBinder mtob = new ManyToOneBinder(mbc, ns, svb, new ManyToOneValuesBinder(), citmto)
+        ForeignKeyOneToOneBinder fkotob = new ForeignKeyOneToOneBinder(mtob, svcf)
+
+        CollectionBinder cb = new CollectionBinder(mbc, ns, svb, etb, mtob, citmto, svcf, ch, collector)
+        PropertyFromValueCreator pfvc = new PropertyFromValueCreator()
+        ComponentUpdater cu = new ComponentUpdater(pfvc)
+        ComponentBinder comb = new ComponentBinder(mbc, binder.getMappingCacheHolder(), cu)
+
+        GrailsPropertyBinder pb = new GrailsPropertyBinder(etb, comb, cb, svb, otob, mtob, fkotob)
+        CompositeIdBinder cib = new CompositeIdBinder(mbc, cu, pb)
+        PropertyBinder pbh = new PropertyBinder()
+        SimpleIdBinder sib = new SimpleIdBinder(mbc, new BasicValueCreator(mbc, je, ns), svb, pbh)
+        IdentityBinder ib = new IdentityBinder(sib, cib)
+        VersionBinder vb = new VersionBinder(mbc, svb, pbh, BasicValue::new)
+
+        ClassBinder clb = new ClassBinder(collector)
+        ClassPropertiesBinder clpb = new ClassPropertiesBinder(pb, pfvc)
+        MultiTenantFilterBinder mtfb = new MultiTenantFilterBinder(new GrailsPropertyResolver(), new MultiTenantFilterDefinitionBinder(), collector, dcnf)
+        JoinedSubClassBinder jscb = new JoinedSubClassBinder(mbc, ns, new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), cnfpapf, clb, collector)
+        UnionSubclassBinder uscb = new UnionSubclassBinder(mbc, ns, clb, collector)
+        SingleTableSubclassBinder stscb = new SingleTableSubclassBinder(clb, mbc)
+
+        SubclassMappingBinder scmb = new SubclassMappingBinder(jscb, uscb, stscb, clpb)
+        SubClassBinder scb = new SubClassBinder(scmb, mtfb, "dataSource")
+        RootPersistentClassCommonValuesBinder rpccvb = new RootPersistentClassCommonValuesBinder(mbc, ns, ib, vb, clb, clpb, collector)
+        DiscriminatorPropertyBinder dpb = new DiscriminatorPropertyBinder(mbc, binder.getMappingCacheHolder(), new ConfiguredDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), new ColumnConfigToColumnBinder()), new DefaultDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder()))
+        RootBinder rb = new RootBinder("default", mtfb, scb, rpccvb, dpb, collector, binder.getMappingCacheHolder())
 
         return [
-            propertyBinder: propertyBinder,
-            collectionBinder: collectionBinder,
-            identityBinder: identityBinder,
-            versionBinder: versionBinder,
-            defaultColumnNameFetcher: defaultColumnNameFetcher,
-            columnNameForPropertyAndPathFetcher: columnNameForPropertyAndPathFetcher,
-            classBinder: classBinder,
-            classPropertiesBinder: classPropertiesBinder,
-            multiTenantFilterBinder: multiTenantFilterBinder,
-            joinedSubClassBinder: joinedSubClassBinder,
-            unionSubclassBinder: unionSubclassBinder,
-            singleTableSubclassBinder: singleTableSubclassBinder,
-            subClassBinder: subClassBinder,
-            rootBinder: rootBinder
+            propertyBinder: pb,
+            collectionBinder: cb,
+            identityBinder: ib,
+            versionBinder: vb,
+            classBinder: clb,
+            classPropertiesBinder: clpb,
+            rootBinder: rb
         ]
     }
 
-    protected void bindRoot(GrailsDomainBinder binder, GrailsHibernatePersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) {
-        def binders = getBinders(binder, mappings)
-        binders.rootBinder.bindRoot(entity)
+    void setupSpec() {
+        // Empty to avoid global failures
+    }
+
+    protected HibernatePersistentProperty propertyFor(Class domainClass, String propertyName) {
+        PersistentEntity entity = createPersistentEntity(domainClass)
+        return (HibernatePersistentProperty) entity.getPropertyByName(propertyName)
     }
 
-    void setupSpec() {
-        manager.addAllDomainClasses([
-            org.apache.grails.data.testing.tck.domains.Pet,
-            org.apache.grails.data.testing.tck.domains.Person,
-            org.apache.grails.data.testing.tck.domains.PetType,
-            LSBAuthor,
-            LSBBook
-        ])
+    protected RootClass createMockPersistentClass(Class domainClass, InFlightMetadataCollector collector, java.util.List<String> properties = []) {
+        def binder = getGrailsDomainBinder()
+        def rootClass = new RootClass(binder.getMetadataBuildingContext())
+        rootClass.setEntityName(domainClass.name)
+        rootClass.setJpaEntityName(domainClass.simpleName)
+        rootClass.setTable(collector.addTable(null, null, domainClass.simpleName.toUpperCase(), null, false, binder.getMetadataBuildingContext()))
+        
+        properties.each { propName ->
+            def p = new Property()
+            p.setName(propName)
+            p.setValue(new BasicValue(binder.getMetadataBuildingContext(), rootClass.getTable()))
+            rootClass.addProperty(p)
+        }
+        
+        collector.addEntityBinding(rootClass)
+        
+        def entity = (GrailsHibernatePersistentEntity) getMappingContext().getPersistentEntity(domainClass.name) ?: createPersistentEntity(domainClass)
+        entity.setPersistentClass(rootClass)
+        
+        return rootClass
     }
 
-    void "Test bind collection"() {
+    def "bindListSecondPass applies index customization"() {
         given:
         def binder = getGrailsDomainBinder()
-        def collectionBinder = getBinders(binder).collectionBinder
         def collector = getCollector()
+        def listBinder = getBinders(binder, collector).collectionBinder.listSecondPassBinder
+        def property = propertyFor(LSBCustomIndex, "items") as HibernateToManyProperty
 
-        def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as GrailsHibernatePersistentEntity
-        def petEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as GrailsHibernatePersistentEntity
+        def rootClass = createMockPersistentClass(LSBCustomIndex, collector)
+        
+        def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), rootClass)
+        list.setRole("${LSBCustomIndex.name}.items".toString())
+        list.setCollectionTable(rootClass.getTable())
+        list.setElement(new BasicValue(binder.getMetadataBuildingContext(), list.getCollectionTable()))
+        property.setCollection(list)
 
-        def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(personEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "PERSON", null, false, binder.getMetadataBuildingContext()))
+        when:
+        listBinder.bindListSecondPass(property)
+
+        then:
+        list.index != null
+        list.index.getColumn(0).name == "my_index_col"
+        (list.index as BasicValue).typeName == "long"
+    }
+
+    def "bindListSecondPass throws exception for many-to-many non-owning side"() {
+        given:
+        def binder = getGrailsDomainBinder()
+        def collector = getCollector()
+        def listBinder = getBinders(binder, collector).collectionBinder.listSecondPassBinder
+        
+        def property = propertyFor(LSBManyToManyB, "owners") as HibernateManyToManyProperty
+        def ownerRoot = createMockPersistentClass(LSBManyToManyB, collector, ["owners"])
 
-        def petsProp = personEntity.getPropertyByName("pets") as HibernateToManyProperty
+        def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), ownerRoot)
+        list.setRole("${LSBManyToManyB.name}.owners".toString())
+        property.setCollection(list)
 
         when:
-        def collection = collectionBinder.bindCollection(petsProp, "")
+        listBinder.bindListSecondPass(property)
 
         then:
-        collection.role == "${personEntity.name}.pets".toString()
-        collection.element instanceof OneToMany
-        (collection.element as OneToMany).referencedEntityName == petEntity.name
+        def e = thrown(MappingException)
+        e.message.contains("has no associated class") || e.message.contains("List collection types only supported on the owning side")
     }
 
-    void "test bidirectional list binding"() {
+    def "bindListSecondPass handles many-to-many specific flags"() {
         given:
         def binder = getGrailsDomainBinder()
         def collector = getCollector()
-        def metadataBuildingContext = binder.getMetadataBuildingContext()
-        def binders = getBinders(binder, collector)
-        def collectionBinder = binders.collectionBinder
-        def listBinder = collectionBinder.listSecondPassBinder
-
-        def authorEntity = getPersistentEntity(LSBAuthor) as GrailsHibernatePersistentEntity
-        def bookEntity = getPersistentEntity(LSBBook) as GrailsHibernatePersistentEntity
-        def booksProp = authorEntity.getPropertyByName("books") as HibernateToManyProperty
-
-        def rootClass = new RootClass(metadataBuildingContext)
-        rootClass.setEntityName(authorEntity.name)
-        rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "LSB_AUTHOR", null, false, metadataBuildingContext))
-        collector.addEntityBinding(rootClass)
+        def listBinder = getBinders(binder, collector).collectionBinder.listSecondPassBinder
+        
+        def property = propertyFor(LSBManyToManyA, "others") as HibernateManyToManyProperty
+        def ownerRoot = createMockPersistentClass(LSBManyToManyA, collector, ["others"])
+        def otherRoot = createMockPersistentClass(LSBManyToManyB, collector, ["owners"])
+
+        def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), ownerRoot)
+        list.setRole("${LSBManyToManyA.name}.others".toString())
+        list.setCollectionTable(collector.addTable(null, null, "JOIN_TABLE", null, false, binder.getMetadataBuildingContext()))
+        list.setKey(new DependantValue(binder.getMetadataBuildingContext(), list.getCollectionTable(), null))
+        list.setElement(new ManyToOne(binder.getMetadataBuildingContext(), list.getCollectionTable()))
+        ((ManyToOne)list.getElement()).setReferencedEntityName(LSBManyToManyB.name)
+        property.setCollection(list)
 
-        def bookRootClass = new RootClass(metadataBuildingContext)
-        bookRootClass.setEntityName(bookEntity.name)
-        bookRootClass.setJpaEntityName(bookEntity.name)
-        bookRootClass.setTable(collector.addTable(null, null, "LSB_BOOK", null, false, metadataBuildingContext))
-        collector.addEntityBinding(bookRootClass)
-
-        def authorValue = new org.hibernate.mapping.ManyToOne(metadataBuildingContext, bookRootClass.getTable())
-        authorValue.setReferencedEntityName(authorEntity.name)
-        def authorCol = new org.hibernate.mapping.Column("author_id")
-        authorCol.setValue(authorValue)
-        authorValue.addColumn(authorCol)
-        def authorProp = new org.hibernate.mapping.Property()
-        authorProp.setName("author")
-        authorProp.setValue(authorValue)
-        bookRootClass.addProperty(authorProp)
-
-        def persistentClasses = [
-            (authorEntity.name): rootClass,
-            (bookEntity.name): bookRootClass
-        ]
+        when:
+        listBinder.bindListSecondPass(property)
+
+        then:
+        def backref = otherRoot.getProperties().find { it.name == "_" + "LSBManyToManyA" + "_" + "others" + "Backref" }
+        backref instanceof Backref
+        !backref.isInsertable()
+
+        def indexBackref = otherRoot.getProperties().find { it.name == "_" + "others" + "IndexBackref" }
+        indexBackref instanceof IndexBackref
+        !indexBackref.isInsertable()
+    }
 
-        def list = new org.hibernate.mapping.List(metadataBuildingContext, rootClass)
-        list.setRole("${authorEntity.name}.books".toString())
+    def "bindListSecondPass handles circular associations"() {
+        given:
+        def binder = getGrailsDomainBinder()
+        def collector = getCollector()
+        def listBinder = getBinders(binder, collector).collectionBinder.listSecondPassBinder
+        def property = propertyFor(LSBCircular, "children") as HibernateToManyProperty
+
+        def rootClass = createMockPersistentClass(LSBCircular, collector, ["parent", "children"])
+
+        def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), rootClass)
+        list.setRole("${LSBCircular.name}.children".toString())
         list.setCollectionTable(rootClass.getTable())
+        def key = new DependantValue(binder.getMetadataBuildingContext(), list.getCollectionTable(), null)
+        key.setNullable(false)
+        list.setKey(key)
+        list.setElement(new ManyToOne(binder.getMetadataBuildingContext(), list.getCollectionTable()))
+        ((ManyToOne)list.getElement()).setReferencedEntityName(LSBCircular.name)
+        property.setCollection(list)
+
+        when:
+        listBinder.bindListSecondPass(property)
 
-        def element = new org.hibernate.mapping.ManyToOne(metadataBuildingContext, list.getCollectionTable())
-        element.setReferencedEntityName(LSBBook.name)
-        list.setElement(element)
+        then:
+        property.isCircular()
+        // For circular, we don't force nullable false
+        list.getKey().isNullable()
+    }
 
-        booksProp.setCollection(list)
+    def "bindListSecondPass handles composite identity"() {
+        given:
+        def binder = getGrailsDomainBinder()
+        def collector = getCollector()
+        def listBinder = getBinders(binder, collector).collectionBinder.listSecondPassBinder
+        
+        def property = propertyFor(LSBCompositeIdOwner, "items") as HibernateToManyProperty
+        def ownerRoot = createMockPersistentClass(LSBCompositeIdOwner, collector, ["items"])
+        def itemRoot = createMockPersistentClass(LSBCompositeIdItem, collector, ["owner", "name"])
+
+        def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), ownerRoot)
+        list.setRole("${LSBCompositeIdOwner.name}.items".toString())
+        list.setCollectionTable(itemRoot.getTable())
+        list.setKey(new DependantValue(binder.getMetadataBuildingContext(), list.getCollectionTable(), null))
+        list.setElement(new ManyToOne(binder.getMetadataBuildingContext(), list.getCollectionTable()))
+        ((ManyToOne)list.getElement()).setReferencedEntityName(LSBCompositeIdItem.name)
+        property.setCollection(list)
+
+        expect:
+        property.getHibernateInverseSide().isCompositeIdProperty()
 
         when:
-        listBinder.bindListSecondPass(booksProp, persistentClasses)
+        listBinder.bindListSecondPass(property)
 
         then:
-        noExceptionThrown()
-        list.index != null
-        list.baseIndex == 0
+        // No Backref should be created for composite ID inverse
+        !itemRoot.getProperties().find { it.name.endsWith("Backref") && it instanceof Backref }
+        
+        // IndexBackref should still be created
+        itemRoot.getProperties().find { it.name == "_" + "items" + "IndexBackref" } instanceof IndexBackref
+    }
+}
+
+@Entity
+class LSBCustomIndex {
+    Long id
+    java.util.List<String> items
+    static hasMany = [items: String]
+    static mapping = {
+        items index: [column: "my_index_col", type: "long"]
     }
 }
 
-@grails.gorm.annotation.Entity
+@Entity
+class LSBCircular {
+    Long id
+    LSBCircular parent
+    java.util.List<LSBCircular> children
+    static hasMany = [children: LSBCircular]
+    static belongsTo = [parent: LSBCircular]
+}
+
+@Entity
 class LSBAuthor {
     Long id
-    List<LSBBook> books
+    java.util.List<LSBBook> books
     static hasMany = [books: LSBBook]
 }
 
-@grails.gorm.annotation.Entity
+@Entity
 class LSBBook {
     Long id
     LSBAuthor author
     static belongsTo = [author: LSBAuthor]
 }
+
+@Entity
+class LSBManyToManyA {
+    Long id
+    java.util.List<LSBManyToManyB> others
+    static hasMany = [others: LSBManyToManyB]
+}
+
+@Entity
+class LSBManyToManyB {
+    Long id
+    java.util.List<LSBManyToManyA> owners
+    static hasMany = [owners: LSBManyToManyA]
+    static belongsTo = LSBManyToManyA
+}
+
+@Entity
+class LSBCompositeIdOwner {
+    Long id
+    java.util.List<LSBCompositeIdItem> items
+    static hasMany = [items: LSBCompositeIdItem]
+}
+
+@Entity
+class LSBCompositeIdItem implements Serializable {
+    LSBCompositeIdOwner owner
+    String name
+    static belongsTo = [owner: LSBCompositeIdOwner]
+    static mapping = {
+        id composite: ['owner', 'name']
+    }
+}