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 1d13ad208cb0b3c901cfb48dd4ed72687025d9c7 Author: Walter B Duque de Estrada <[email protected]> AuthorDate: Fri Jan 16 10:39:02 2026 -0600 update progress --- .../core/HIBERNATE7-UPGRADE-PROGRESS.md | 20 ++- grails-data-hibernate7/core/build.gradle | 1 + .../orm/hibernate/cfg/GrailsDomainBinder.java | 77 +------- .../cfg/GrailsSequenceStyleGenerator.java | 29 +++ .../cfg/domainbinding/BasicValueIdCreator.java | 68 +++++++ .../cfg/domainbinding/SimpleIdBinder.java | 65 +++++++ .../domainbinding/BasicValueIdCreatorSpec.groovy | 199 +++++++++++++++++++++ .../cfg/domainbinding/SimpleIdBinderSpec.groovy | 114 ++++++++++++ 8 files changed, 499 insertions(+), 74 deletions(-) diff --git a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md index 501a9db689..3186baf474 100644 --- a/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md +++ b/grails-data-hibernate7/core/HIBERNATE7-UPGRADE-PROGRESS.md @@ -49,10 +49,26 @@ This document summarizes the approaches taken, challenges encountered, and futur - **Reasoning:** Hibernate 7's default naming strategies preserve dots in logical names (e.g., from FQCNs), which leads to invalid SQL in databases like H2. GORM expects underscores for compatibility and valid SQL. - **Result:** Resolved `JdbcSQLSyntaxErrorException` in tests like `CascadeBehaviorPersisterSpec`, where join table columns now use valid underscore-delimited names instead of dotted class names. +### 8. Refactoring of `AbstractGrailsDomainBinder` +- **Approach:** Merged `AbstractGrailsDomainBinder` into `GrailsDomainBinder` to simplify the class hierarchy and remove the abstract base class. +- **Result:** `AbstractGrailsDomainBinder` has been removed and its functionality (mapping cache management) is now part of `GrailsDomainBinder`. + +### 9. Test Fixes for Sealed Classes +- **Issue:** Tests were failing with "Sealed class ... cannot be mocked" for Hibernate classes like `Column`, `Table`, and `UniqueKey`. +- **Approach:** Refactored tests (`ColumnConfigToColumnBinderSpec`, `StringColumnConstraintsBinderSpec`, `IndexBinderSpec`, `UniqueNameGeneratorSpec`) to instantiate these classes instead of mocking them. +- **Result:** Resolved mocking errors for sealed classes. + +### 10. Fixes for `BasicValueIdCreatorSpec` and `ManyToOneBinderSpec` +- **Issue:** `BasicValueIdCreatorSpec` failed with NPE in `NativeGenerator`. `ManyToOneBinderSpec` failed with `MissingMethodException`. +- **Approach:** + - In `BasicValueIdCreatorSpec`, mocked `Database`, `Dialect`, and `GenerationType` to satisfy `NativeGenerator` initialization. + - In `ManyToOneBinderSpec`, replaced `setCompositeIdentifier` with `setIdentity` and mocked `PropertyConfig` to handle `setUniqueWithinGroup`. +- **Result:** Resolved these specific test failures. + ## Challenges & Failures ### 1. Proxy Initialization Behavior -- **Issue:** In `Hibernate6GroovyProxySpec`, `Location.proxy(id)` returns an object that is already initialized (`Hibernate.isInitialized(location) == true`), even after clearing the session. +- **Issue:** In `Hibernate7GroovyProxySpec`, `Location.proxy(id)` returns an object that is already initialized (`Hibernate.isInitialized(location) == true`), even after clearing the session. - **Attempts:** Tried `session.getReference()`, `session.byId().getReference()`, and using fresh sessions. - **Status:** Ongoing investigation. Debugging indicates that Hibernate 7's bytecode enhancement or session management might be reporting Groovy proxies as initialized even when they haven't fetched their target. @@ -85,7 +101,7 @@ Unit tests should be created for each new binder class (e.g., `CollectionBinderS ## Future Steps -1. **Resolve Proxy Initialization:** Determine why proxies are returning as initialized in `Hibernate6GroovyProxySpec`. Investigate if Hibernate 7's bytecode enhancement or ByteBuddy factory settings are interfering. +1. **Resolve Proxy Initialization:** Determine why proxies are returning as initialized in `Hibernate7GroovyProxySpec`. Investigate if Hibernate 7's bytecode enhancement or ByteBuddy factory settings are interfering. 2. **Fix DDL Generation:** Investigate why FQCNs are leaking into DDL column definitions. This likely requires further changes in `ColumnNameFetcher` or the mapping binders to ensure dots are replaced by underscores globally for generated columns. 3. Continue TCK Failure Audit: - `HibernateGormDatastoreSpec` (Base class, not directly runnable - Pending) diff --git a/grails-data-hibernate7/core/build.gradle b/grails-data-hibernate7/core/build.gradle index 1b63057321..840f888c28 100644 --- a/grails-data-hibernate7/core/build.gradle +++ b/grails-data-hibernate7/core/build.gradle @@ -99,6 +99,7 @@ dependencies { testImplementation 'org.apache.groovy:groovy-json' testImplementation 'org.hibernate.orm:hibernate-jcache' testImplementation 'org.spockframework:spock-core' + testImplementation "org.hibernate.orm:hibernate-core:$hibernate7Version" // groovy proxy fixes bytebuddy to be a bit smarter when it comes to groovy metaClass testImplementation "org.yakworks:hibernate-groovy-proxy:$yakworksHibernateGroovyProxyVersion", { 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 4a36a3e597..78c584b852 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 @@ -36,6 +36,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.PersistentPropertyToPropertyCo import org.grails.orm.hibernate.cfg.domainbinding.SimpleValueColumnBinder; import org.grails.orm.hibernate.cfg.domainbinding.TypeNameProvider; import org.grails.orm.hibernate.cfg.domainbinding.*; + import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.boot.ResourceStreamLocator; @@ -94,8 +95,8 @@ 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.Optional; import java.util.SortedSet; import java.util.StringTokenizer; @@ -118,7 +119,7 @@ public class GrailsDomainBinder public static final String FOREIGN_KEY_SUFFIX = "_id"; protected static final Map<Class<?>, Mapping> MAPPING_CACHE = new HashMap<>(); private static final String STRING_TYPE = "string"; - private static final String EMPTY_PATH = ""; + public static final String EMPTY_PATH = ""; public static final char UNDERSCORE = '_'; public static final String BACKTICK = "`"; @@ -2009,77 +2010,9 @@ public class GrailsDomainBinder @SuppressWarnings("unchecked") private void bindSimpleId(PersistentProperty identifier, RootClass entity, InFlightMetadataCollector mappings, Identity mappedId, String sessionFactoryBeanName) { + SimpleIdBinder simpleIdBinder = new SimpleIdBinder(metadataBuildingContext,namingStrategy); + simpleIdBinder.bindSimpleId(identifier, entity, mappedId); - boolean useSequence = new HibernateEntityWrapper().getMappedForm(identifier.getOwner()).isTablePerConcreteClass(); - // create the id value - BasicValue id = new BasicValue(metadataBuildingContext, entity.getTable()); - - String generator; - if (mappedId == null) { - generator = useSequence ? "sequence-identity" : "native"; - } else { - generator = mappedId.getGenerator(); - if ("native".equals(generator) && useSequence) { - generator = "sequence-identity"; - } - } - - switch (generator) { - case "identity" -> id.setCustomIdGeneratorCreator(context -> { - // Force IdentityGenerator for databases like MySQL/H2 - var gen = new org.hibernate.id.IdentityGenerator(); - context.getProperty().getValue().getColumns().get(0).setIdentity(true); - return gen; - }); - - case "sequence", "sequence-identity" -> id.setCustomIdGeneratorCreator(context -> { - // Use the modern SequenceStyleGenerator - return new org.hibernate.id.enhanced.SequenceStyleGenerator(); - }); - - case "increment" -> id.setCustomIdGeneratorCreator(context -> { - return new org.hibernate.id.IncrementGenerator(); - }); - - case "uuid", "uuid2" -> id.setCustomIdGeneratorCreator(context -> { - return new org.hibernate.id.uuid.UuidGenerator(context.getType().getReturnedClass()); - }); - - case "assigned" -> id.setCustomIdGeneratorCreator(context -> { - return new org.hibernate.id.Assigned(); - }); - - case "table", "enhanced-table" -> id.setCustomIdGeneratorCreator(context -> { - return new org.hibernate.id.enhanced.TableGenerator(); - }); - - case "hilo" -> id.setCustomIdGeneratorCreator(context -> { - // Note: Legacy Hilo is often replaced by SequenceStyleGenerator with optimizer - return new org.hibernate.id.enhanced.SequenceStyleGenerator(); - }); - - default -> id.setCustomIdGeneratorCreator(GrailsNativeGenerator::new); - } - - Property idProperty = new Property(); - idProperty.setName(identifier.getName()); - idProperty.setValue(id); - entity.setDeclaredIdentifierProperty(idProperty); - entity.setIdentifier(id); - // set type - new SimpleValueBinder(namingStrategy).bindSimpleValue(identifier, null, id, EMPTY_PATH); - - // create property - Property prop = new Property(); - prop.setValue(id); - - // bind property - new PropertyBinder().bindProperty(identifier, prop); - // set identifier property - entity.setIdentifierProperty(prop); - - Table table = id.getTable(); - table.setPrimaryKey(new PrimaryKey(table)); } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsSequenceStyleGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsSequenceStyleGenerator.java new file mode 100644 index 0000000000..3d8938cf47 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsSequenceStyleGenerator.java @@ -0,0 +1,29 @@ +package org.grails.orm.hibernate.cfg; + +import org.hibernate.MappingException; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.id.enhanced.SequenceStyleGenerator; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.Type; + +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +public class GrailsSequenceStyleGenerator extends SequenceStyleGenerator { + + public GrailsSequenceStyleGenerator(GeneratorCreationContext context, Identity mappedId) { + // Call super's no-arg constructor first + super(); + Properties generatorProps = new Properties(); + if (mappedId != null && mappedId.getParams() != null) { + for (Map.Entry entry : (Set<Map.Entry>) mappedId.getParams().entrySet()) { + generatorProps.setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + } + super.configure(context.getType(), generatorProps, context.getServiceRegistry()); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreator.java new file mode 100644 index 0000000000..1915943aba --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreator.java @@ -0,0 +1,68 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.RootClass; + +import org.grails.orm.hibernate.cfg.GrailsSequenceStyleGenerator; +import org.grails.orm.hibernate.cfg.Identity; + +public class BasicValueIdCreator { + + private final MetadataBuildingContext metadataBuildingContext; + + public BasicValueIdCreator(MetadataBuildingContext metadataBuildingContext ) { + this.metadataBuildingContext = metadataBuildingContext; + } + + public BasicValue getBasicValueId(RootClass entity, Identity mappedId, boolean useSequence) { + BasicValue id = new BasicValue(metadataBuildingContext, entity.getTable()); + + String generator; + if (mappedId == null) { + generator = useSequence ? "sequence-identity" : "native"; + } else { + generator = mappedId.getGenerator(); + if ("native".equals(generator) && useSequence) { + generator = "sequence-identity"; + } + } + + switch (generator) { + case "identity" -> id.setCustomIdGeneratorCreator(context -> { + // Force IdentityGenerator for databases like MySQL/H2 + var gen = new org.hibernate.id.IdentityGenerator(); + context.getProperty().getValue().getColumns().get(0).setIdentity(true); + return gen; + }); + + case "sequence", "sequence-identity" -> id.setCustomIdGeneratorCreator(context -> { + return new GrailsSequenceStyleGenerator(context,mappedId); + }); + + case "increment" -> id.setCustomIdGeneratorCreator(context -> { + return new org.hibernate.id.IncrementGenerator(); + }); + + case "uuid", "uuid2" -> id.setCustomIdGeneratorCreator(context -> { + return new org.hibernate.id.uuid.UuidGenerator(context.getType().getReturnedClass()); + }); + + case "assigned" -> id.setCustomIdGeneratorCreator(context -> { + return new org.hibernate.id.Assigned(); + }); + + case "table", "enhanced-table" -> id.setCustomIdGeneratorCreator(context -> { + return new org.hibernate.id.enhanced.TableGenerator(); + }); + + case "hilo" -> id.setCustomIdGeneratorCreator(context -> { + // Note: Legacy Hilo is often replaced by SequenceStyleGenerator with optimizer + return new org.hibernate.id.enhanced.SequenceStyleGenerator(); + }); + + default -> id.setCustomIdGeneratorCreator(GrailsNativeGenerator::new); + } + return id; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinder.java new file mode 100644 index 0000000000..e48b5251d2 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinder.java @@ -0,0 +1,65 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.PrimaryKey; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.RootClass; +import org.hibernate.mapping.Table; + +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.orm.hibernate.cfg.Identity; +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; + +import static org.grails.orm.hibernate.cfg.GrailsDomainBinder.EMPTY_PATH; + +public class SimpleIdBinder { + + private final BasicValueIdCreator basicValueIdCreator; + private final HibernateEntityWrapper hibernateEntityWrapper; + private final SimpleValueBinder simpleValueBinder; + private final PropertyBinder propertyBinder; + + public SimpleIdBinder(MetadataBuildingContext metadataBuildingContext, PersistentEntityNamingStrategy namingStrategy) { + this.basicValueIdCreator = new BasicValueIdCreator(metadataBuildingContext); + this.hibernateEntityWrapper = new HibernateEntityWrapper(); + this.simpleValueBinder =new SimpleValueBinder(namingStrategy); + this.propertyBinder = new PropertyBinder(); + } + + protected SimpleIdBinder(BasicValueIdCreator basicValueIdCreate, HibernateEntityWrapper hibernateEntityWrapper, SimpleValueBinder simpleValueBinder, PropertyBinder propertyBinder) { + this.basicValueIdCreator = basicValueIdCreate; + this.hibernateEntityWrapper = hibernateEntityWrapper; + this.simpleValueBinder = simpleValueBinder; + this.propertyBinder = propertyBinder; + } + + + public void bindSimpleId(PersistentProperty identifier, RootClass entity, Identity mappedId) { + + boolean useSequence = hibernateEntityWrapper.getMappedForm(identifier.getOwner()).isTablePerConcreteClass(); + // create the id value + + BasicValue id = basicValueIdCreator.getBasicValueId(entity, mappedId, useSequence); + + Property idProperty = new Property(); + idProperty.setName(identifier.getName()); + idProperty.setValue(id); + entity.setDeclaredIdentifierProperty(idProperty); + entity.setIdentifier(id); + // set type + simpleValueBinder.bindSimpleValue(identifier, null, id, EMPTY_PATH); + + // create property + Property prop = new Property(); + prop.setValue(id); + + // bind property + propertyBinder.bindProperty(identifier, prop); + // set identifier property + entity.setIdentifierProperty(prop); + + Table table = id.getTable(); + table.setPrimaryKey(new PrimaryKey(table)); + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreatorSpec.groovy new file mode 100644 index 0000000000..5b47980b64 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueIdCreatorSpec.groovy @@ -0,0 +1,199 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import grails.gorm.specs.HibernateGormDatastoreSpec +import jakarta.persistence.GenerationType +import org.grails.orm.hibernate.cfg.GrailsSequenceStyleGenerator +import org.grails.orm.hibernate.cfg.Identity +import org.hibernate.boot.model.naming.Identifier +import org.hibernate.boot.model.relational.Database +import org.hibernate.boot.spi.MetadataBuildingContext +import org.hibernate.dialect.Dialect +import org.hibernate.dialect.sequence.SequenceSupport +import org.hibernate.engine.config.spi.ConfigurationService +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment +import org.hibernate.generator.Generator +import org.hibernate.generator.GeneratorCreationContext +import org.hibernate.id.Assigned +import org.hibernate.id.IdentityGenerator +import org.hibernate.id.IncrementGenerator +import org.hibernate.id.enhanced.SequenceStyleGenerator +import org.hibernate.id.enhanced.TableGenerator +import org.hibernate.id.uuid.UuidGenerator +import org.hibernate.mapping.BasicValue +import org.hibernate.mapping.Column +import org.hibernate.mapping.Property +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.Table +import org.hibernate.mapping.Value +import org.hibernate.service.ServiceRegistry +import org.hibernate.type.BasicType +import spock.lang.Unroll + +class BasicValueIdCreatorSpec extends HibernateGormDatastoreSpec { + + MetadataBuildingContext metadataBuildingContext + BasicValueIdCreator creator + RootClass entity + Table table + + def setup() { + metadataBuildingContext = getGrailsDomainBinder().getMetadataBuildingContext() + creator = new BasicValueIdCreator(metadataBuildingContext) + entity = new RootClass(metadataBuildingContext) + table = new Table("test_table") + entity.setTable(table) + } + + @Unroll + def "should create BasicValue with correct generator for #generatorName (useSequence: #useSequence)"() { + given: + Identity mappedId = new Identity() + mappedId.setGenerator(generatorName) + def property = createDummyProperty() + + when: + BasicValue id = creator.getBasicValueId(entity, mappedId, useSequence) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(createContext(id, property)) + + then: + expectedClass.isInstance(generator) + + where: + generatorName | useSequence | expectedClass + "identity" | false | IdentityGenerator + "sequence" | true | GrailsSequenceStyleGenerator + "sequence-identity" | true | GrailsSequenceStyleGenerator + "increment" | false | IncrementGenerator + "uuid" | false | UuidGenerator + "uuid2" | false | UuidGenerator + "assigned" | false | Assigned + "table" | false | TableGenerator + "enhanced-table" | false | TableGenerator + "hilo" | false | SequenceStyleGenerator + } + + def "should default to native generator when mappedId is null"() { + when: + BasicValue id = creator.getBasicValueId(entity, null, false) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(createContext(id)) + + then: + generator instanceof GrailsNativeGenerator + } + + def "should default to sequence-identity when mappedId is null and useSequence is true"() { + when: + BasicValue id = creator.getBasicValueId(entity, null, true) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(createContext(id)) + + then: + generator instanceof GrailsSequenceStyleGenerator + } + + def "should use sequence-identity when generator is native and useSequence is true"() { + given: + Identity mappedId = new Identity() + mappedId.setGenerator("native") + + when: + BasicValue id = creator.getBasicValueId(entity, mappedId, true) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(createContext(id)) + + then: + generator instanceof GrailsSequenceStyleGenerator + } + + def "should configure identity column for identity generator"() { + given: + Identity mappedId = new Identity() + mappedId.setGenerator("identity") + // We need a real column structure for IdentityGenerator to work with + def column = new Column("id") + + // Mocking the context to simulate what Hibernate passes + def property = Mock(Property) + def value = Mock(Value) // We can mock Value interface + value.getColumns() >> [column] + property.getValue() >> value + + when: + BasicValue id = creator.getBasicValueId(entity, mappedId, false) + def generatorCreator = id.getCustomIdGeneratorCreator() + def context = createContext(id, property) + + generatorCreator.createGenerator(context) + + then: + column.isIdentity() + } + + def "should pass generator properties to sequence generator"() { + given: + Identity mappedId = new Identity() + mappedId.setGenerator("sequence") + mappedId.setParams([sequence_name: "my_seq"]) + + when: + BasicValue id = creator.getBasicValueId(entity, mappedId, true) + def generatorCreator = id.getCustomIdGeneratorCreator() + GrailsSequenceStyleGenerator generator = (GrailsSequenceStyleGenerator) generatorCreator.createGenerator(createContext(id)) + + then: + generator.getDatabaseStructure().getPhysicalName().getObjectName().getText() == "my_seq" + } + + private Property createDummyProperty() { + def column = new Column("id") + def property = Mock(Property) + def value = Mock(Value) + value.getColumns() >> [column] + property.getValue() >> value + return property + } + + private GeneratorCreationContext createContext(BasicValue id, Property property = null) { + def context = Mock(GeneratorCreationContext) + def type = Mock(BasicType) + def database = Mock(Database) + def dialect = Mock(Dialect) + def serviceRegistry = Mock(ServiceRegistry) + def jdbcEnvironment = Mock(JdbcEnvironment) + def identifierHelper = Mock(IdentifierHelper) + def sequenceSupport = Mock(SequenceSupport) + def configurationService = Mock(ConfigurationService) + + type.getReturnedClass() >> String.class + context.getType() >> type + + // Mocking for NativeGenerator + context.getDatabase() >> database + database.getDialect() >> dialect + dialect.getNativeValueGenerationStrategy() >> GenerationType.SEQUENCE + dialect.getSequenceSupport() >> sequenceSupport + sequenceSupport.supportsSequences() >> true + + // Mocking for SequenceStyleGenerator + context.getServiceRegistry() >> serviceRegistry + serviceRegistry.requireService(JdbcEnvironment.class) >> jdbcEnvironment + serviceRegistry.requireService(ConfigurationService.class) >> configurationService + jdbcEnvironment.getDialect() >> dialect + jdbcEnvironment.getIdentifierHelper() >> identifierHelper + identifierHelper.toIdentifier(_, _) >> { String text, boolean quoted -> + return text == null ? null : new Identifier(text, quoted) + } + identifierHelper.toIdentifier(_) >> { String text -> + return text == null ? null : new Identifier(text, false) + } + + if (property != null) { + context.getProperty() >> property + } + + return context + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy new file mode 100644 index 0000000000..b9029822ea --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy @@ -0,0 +1,114 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import grails.persistence.Entity +import org.grails.orm.hibernate.cfg.Identity +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy +import org.hibernate.boot.spi.MetadataBuildingContext +import org.hibernate.mapping.BasicValue +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.Table +import spock.lang.Issue + +class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { + + def metadataBuildingContext + def namingStrategy + def hibernateEntityWrapper + def simpleValueBinder + def propertyBinder + def basicValueIdCreator + + def simpleIdBinder // Subject under test + + def setup() { + metadataBuildingContext = getGrailsDomainBinder().getMetadataBuildingContext() + namingStrategy = Mock(PersistentEntityNamingStrategy) + hibernateEntityWrapper = Mock(HibernateEntityWrapper) + simpleValueBinder = Mock(SimpleValueBinder) + propertyBinder = Mock(PropertyBinder) + // Use real BasicValueIdCreator instead of Mock to avoid issues + basicValueIdCreator = new BasicValueIdCreator(metadataBuildingContext) + + simpleIdBinder = new SimpleIdBinder(basicValueIdCreator, hibernateEntityWrapper, simpleValueBinder, propertyBinder) + } + + + def "test bindSimpleId method with identity generator"() { + given: + def testEntity = createPersistentEntity(TestEntityId) + assert testEntity != null + def rootClass = new RootClass(metadataBuildingContext) + def table = new Table("TEST_TABLE") + rootClass.setTable(table) + + def mappedId = new Identity(generator: "identity") + def testProperty = testEntity.getIdentity() + + // Stubbing interactions + hibernateEntityWrapper.getMappedForm(_) >> Mock(org.grails.orm.hibernate.cfg.Mapping) { + isTablePerConcreteClass() >> false + } + + when: + simpleIdBinder.bindSimpleId(testProperty, rootClass, mappedId) + + then: + 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, '') + 1 * propertyBinder.bindProperty(testProperty, _) + + rootClass.identifier instanceof BasicValue + rootClass.declaredIdentifierProperty != null + rootClass.identifierProperty != null + rootClass.table.primaryKey != null + } + + def "test bindSimpleId method with sequence generator"() { + given: + def testEntity = createPersistentEntity(TestEntitySeq) + assert testEntity != null + def rootClass = new RootClass(metadataBuildingContext) + def table = new Table("TEST_TABLE") + rootClass.setTable(table) + + def mappedId = new Identity(generator: "sequence", params: [sequence: "SEQ_TEST"]) + def testProperty = testEntity.getIdentity() + + // Stubbing interactions + hibernateEntityWrapper.getMappedForm(_) >> Mock(org.grails.orm.hibernate.cfg.Mapping) { + isTablePerConcreteClass() >> true + } + + when: + simpleIdBinder.bindSimpleId(testProperty, rootClass, mappedId) + + then: + 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, '') + 1 * propertyBinder.bindProperty(testProperty, _) + + rootClass.identifier instanceof BasicValue + rootClass.declaredIdentifierProperty != null + rootClass.identifierProperty != null + rootClass.table.primaryKey != null + } +} + +// Helper domain classes +@Entity +class TestEntityId { + Long id + String name + static mapping = { + id generator: 'identity' + } +} + +@Entity +class TestEntitySeq { + Long id + String name + static mapping = { + id generator: 'sequence', params: [sequence: 'SEQ_TEST'] + } +}
