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/03/14 22:49:32 UTC
(grails-core) 04/15: hibernate 7: Ensuring robust identifier handling through the creation of synthetic ID properties for entities that lack explicit identifier definitions
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 <wb...@mac.com>
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)
}
}
+
+@jakarta.persistence.Entity
+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 {}