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 {}

Reply via email to