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 10b6d57321d29838b8be6eff7a98ee6af524e345 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Mar 12 11:48:39 2026 -0500 hibernate 7: Ensuring robust identifier handling through the creation of synthetic ID properties for entities that lack explicit identifier definitions --- .../cfg/domainbinding/binder/SimpleIdBinder.java | 10 ++ .../hibernate/HibernateIdentityProperty.java | 4 + .../cfg/domainbinding/IdentityBinderSpec.groovy | 21 +++ grails-datastore-core/build.gradle | 2 + .../model/types/mapping/IdentityWithMapping.java | 4 + .../GormMappingConfigurationStrategySpec.groovy | 187 ++++++++++++++++++++- 6 files changed, 224 insertions(+), 4 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java index 8d795443e4..fbe30683c9 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java @@ -28,9 +28,13 @@ import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.Table; +import org.grails.datastore.mapping.model.DefaultPropertyMapping; +import org.grails.datastore.mapping.model.config.GormProperties; import org.grails.orm.hibernate.cfg.Identity; import org.grails.orm.hibernate.cfg.Mapping; +import org.grails.orm.hibernate.cfg.PropertyConfig; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentityProperty; import org.grails.orm.hibernate.cfg.domainbinding.util.BasicValueIdCreator; import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.EMPTY_PATH; @@ -69,6 +73,12 @@ public class SimpleIdBinder { basicValueIdCreator.getBasicValueId(metadataBuildingContext, table, mappedId, domainClass, useSequence); var identifier = domainClass.getIdentity(); + if (identifier == null) { + var syntheticId = new HibernateIdentityProperty( + domainClass, domainClass.getMappingContext(), GormProperties.IDENTITY, Long.class); + syntheticId.setMapping(new DefaultPropertyMapping<>(domainClass.getMapping(), new PropertyConfig())); + identifier = syntheticId; + } if (mappedId != null) { String propertyName = mappedId.getName(); if (propertyName != null && !propertyName.equals(domainClass.getName())) { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateIdentityProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateIdentityProperty.java index 0037fd8f21..07bdc9ca1f 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateIdentityProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateIdentityProperty.java @@ -31,4 +31,8 @@ public class HibernateIdentityProperty extends IdentityWithMapping<PropertyConfi public HibernateIdentityProperty(PersistentEntity entity, MappingContext context, PropertyDescriptor property) { super(entity, context, property); } + + public HibernateIdentityProperty(PersistentEntity entity, MappingContext context, String name, Class type) { + super(entity, context, name, type); + } } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/IdentityBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/IdentityBinderSpec.groovy index be605861a2..5650e0b8a2 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/IdentityBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/IdentityBinderSpec.groovy @@ -1,6 +1,8 @@ package org.grails.orm.hibernate.cfg.domainbinding +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.ClassMapping import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty import org.grails.orm.hibernate.cfg.CompositeIdentity import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity @@ -139,4 +141,23 @@ class IdentityBinderSpec extends HibernateGormDatastoreSpec { identity.getName() == "MyEntity" 1 * simpleIdBinder.bindSimpleId(domainClass, root, identity, _) } + + def "should create synthetic identifier property if it doesn't exist"() { + given: + def domainClass = Mock(GrailsHibernatePersistentEntity) + def root = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) + def mappings = Mock(InFlightMetadataCollector) + def identity = new Identity() + domainClass.getHibernateIdentity() >> identity + domainClass.getIdentity() >> null + domainClass.getName() >> "MyEntity" + domainClass.getMappingContext() >> getGrailsDomainBinder().hibernateMappingContext + domainClass.getMapping() >> Mock(ClassMapping) + + when: + binder.bindIdentity(domainClass, root) + + then: + 1 * simpleIdBinder.bindSimpleId(domainClass, root, identity, _) + } } diff --git a/grails-datastore-core/build.gradle b/grails-datastore-core/build.gradle index bd0e6835e0..26dbdb8083 100644 --- a/grails-datastore-core/build.gradle +++ b/grails-datastore-core/build.gradle @@ -88,6 +88,8 @@ dependencies { // There are some tests that use JUnit 5 } testImplementation 'org.spockframework:spock-core' + testImplementation 'net.bytebuddy:byte-buddy' + testImplementation 'org.objenesis:objenesis' testRuntimeOnly 'org.apache.groovy:groovy-test-junit5' testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during tests diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/mapping/IdentityWithMapping.java b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/mapping/IdentityWithMapping.java index de3a6aa2a7..3801f92151 100644 --- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/mapping/IdentityWithMapping.java +++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/mapping/IdentityWithMapping.java @@ -42,6 +42,10 @@ public class IdentityWithMapping<T extends Property> extends Identity<T> impleme super(entity, context, property); } + public IdentityWithMapping(PersistentEntity entity, MappingContext context, String name, Class type) { + super(entity, context, name, type); + } + public IdentityWithMapping(PersistentEntity entity, MappingContext context, PropertyDescriptor property, PropertyMapping<T> propertyMapping) { super(entity, context, property); this.propertyMapping = propertyMapping; diff --git a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/model/config/GormMappingConfigurationStrategySpec.groovy b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/model/config/GormMappingConfigurationStrategySpec.groovy index 07a36f0531..e6559fcb60 100644 --- a/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/model/config/GormMappingConfigurationStrategySpec.groovy +++ b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/model/config/GormMappingConfigurationStrategySpec.groovy @@ -18,12 +18,32 @@ */ package org.grails.datastore.mapping.model.config +import grails.gorm.annotation.Entity import org.grails.datastore.mapping.keyvalue.mapping.config.GormKeyValueMappingFactory +import org.grails.datastore.mapping.model.ClassMapping +import org.grails.datastore.mapping.model.IdentityMapping +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.MappingFactory +import org.grails.datastore.mapping.model.PersistentEntity import org.grails.datastore.mapping.reflect.ClassPropertyFetcher import spock.lang.Specification +import java.beans.PropertyDescriptor class GormMappingConfigurationStrategySpec extends Specification { + void "test isPersistentEntity"() { + given: + def strategy = new GormMappingConfigurationStrategy(new GormKeyValueMappingFactory("test")) + + expect: + strategy.isPersistentEntity(AnnotatedEntity) + strategy.isPersistentEntity(GormAnnotatedEntity) + !strategy.isPersistentEntity(NotAnEntity) + !strategy.isPersistentEntity(null) + !strategy.isPersistentEntity(EnumEntity) + !strategy.isPersistentEntity(Closure) + } + void "test getAssociationMap subclass overrides parent"() { ClassPropertyFetcher cpf = ClassPropertyFetcher.forClass(B) def strategy = new GormMappingConfigurationStrategy(new GormKeyValueMappingFactory("test")) @@ -36,10 +56,169 @@ class GormMappingConfigurationStrategySpec extends Specification { associations.get("foo") == Integer } - class A { - static hasMany = [foo: String] + void "test getIdentity"() { + given: + def mappingFactory = Mock(MappingFactory) + def strategy = new GormMappingConfigurationStrategy(mappingFactory) + def context = Mock(MappingContext) + def entity = Mock(PersistentEntity) + def classMapping = Mock(ClassMapping) + def identityMapping = Mock(IdentityMapping) + + context.getPersistentEntity(SimpleIdEntity.name) >> entity + entity.getJavaClass() >> SimpleIdEntity + entity.getMapping() >> classMapping + classMapping.getIdentifier() >> identityMapping + identityMapping.getIdentifierName() >> (['id'] as String[]) + + when: + strategy.getIdentity(SimpleIdEntity, context) + + then: + 1 * mappingFactory.createIdentity(entity, context, _) + } + + void "test getCompositeIdentity"() { + given: + def mappingFactory = Mock(MappingFactory) + def strategy = new GormMappingConfigurationStrategy(mappingFactory) + def context = Mock(MappingContext) + def entity = Mock(PersistentEntity) + def classMapping = Mock(ClassMapping) + def identityMapping = Mock(IdentityMapping) + + context.getPersistentEntity(CompositeIdEntity.name) >> entity + entity.getJavaClass() >> CompositeIdEntity + entity.getMapping() >> classMapping + classMapping.getIdentifier() >> identityMapping + identityMapping.getIdentifierName() >> (['id1', 'id2'] as String[]) + entity.getPropertyByName('id1') >> null + entity.getPropertyByName('id2') >> null + + when: + strategy.getCompositeIdentity(CompositeIdEntity, context) + + then: + 2 * mappingFactory.createIdentity(entity, context, _) + } + + void "test getPersistentProperties with basic properties and transients"() { + given: + def mappingFactory = Mock(MappingFactory) + def strategy = new GormMappingConfigurationStrategy(mappingFactory) + def context = Mock(MappingContext) + def entity = Mock(PersistentEntity) + + entity.getJavaClass() >> PropertyEntity + mappingFactory.isSimpleType(String) >> true + mappingFactory.isSimpleType(Integer) >> true + mappingFactory.createPropertyDescriptor(_, _) >> { Class cls, mp -> + new PropertyDescriptor(mp.name, cls, "get${mp.name.capitalize()}", "set${mp.name.capitalize()}") + } + + when: + def props = strategy.getPersistentProperties(entity, context, null) + + then: + props.size() == 2 + 1 * mappingFactory.createSimple(entity, context, { it.name == 'name' }) + 1 * mappingFactory.createSimple(entity, context, { it.name == 'age' }) + 0 * mappingFactory.createSimple(entity, context, { it.name == 'transientProp' }) + } + + void "test getIdentity returns null when no identity is present"() { + given: + def mappingFactory = Mock(MappingFactory) + def strategy = new GormMappingConfigurationStrategy(mappingFactory) + def context = Mock(MappingContext) + def entity = Mock(PersistentEntity) + def classMapping = Mock(ClassMapping) + def identityMapping = Mock(IdentityMapping) + + context.getPersistentEntity(NoIdEntity.name) >> entity + entity.getJavaClass() >> NoIdEntity + entity.getMapping() >> classMapping + classMapping.getIdentifier() >> identityMapping + identityMapping.getIdentifierName() >> ([] as String[]) + + when: + def result = strategy.getIdentity(NoIdEntity, context) + + then: + result == null + } + + void "test getCompositeIdentity returns empty array when no identity is present"() { + given: + def mappingFactory = Mock(MappingFactory) + def strategy = new GormMappingConfigurationStrategy(mappingFactory) + def context = Mock(MappingContext) + def entity = Mock(PersistentEntity) + def classMapping = Mock(ClassMapping) + def identityMapping = Mock(IdentityMapping) + + context.getPersistentEntity(NoIdEntity.name) >> entity + entity.getJavaClass() >> NoIdEntity + entity.getMapping() >> classMapping + classMapping.getIdentifier() >> identityMapping + identityMapping.getIdentifierName() >> ([] as String[]) + + when: + def result = strategy.getCompositeIdentity(NoIdEntity, context) + + then: + result.length == 0 } - class B extends A { - static hasMany = [foo: Integer] + + void "test getOwningEntities"() { + given: + def strategy = new GormMappingConfigurationStrategy(Mock(MappingFactory)) + + when: + def owners = strategy.getOwningEntities(ChildEntity, Mock(MappingContext)) + + then: + owners.size() == 1 + owners.contains(ParentEntity) } } + [email protected] +class AnnotatedEntity {} + +@Entity +class GormAnnotatedEntity {} + +class NotAnEntity {} + +enum EnumEntity { FIRST } + +class A { + static hasMany = [foo: String] +} +class B extends A { + static hasMany = [foo: Integer] +} + +class SimpleIdEntity { + Long id +} + +class CompositeIdEntity { + Long id1 + Long id2 +} + +class PropertyEntity { + String name + Integer age + String transientProp + static transients = ['transientProp'] +} + +class ParentEntity {} +class ChildEntity { + static belongsTo = [parent: ParentEntity] +} + +class NoIdEntity {}
