You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/02 18:16:53 UTC
(grails-core) 01/01: Restore mapped identifiers in GORM ident()
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch fix/composite-ident
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 055cc1243fd5b269d165ca039a4f97f6b32402f0
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Oct 2 14:16:41 2026 -0400
Restore mapped identifiers in GORM ident()
---
.../grails/gorm/tests/MappedIdentifierSpec.groovy | 140 +++++++++++++++++++++
.../grails/gorm/tests/MappedIdentifierSpec.groovy | 140 +++++++++++++++++++++
.../grails/datastore/gorm/GormInstanceApi.groovy | 22 +++-
.../datastore/gorm/GormInstanceApiSpec.groovy | 5 +-
grails-doc/src/en/ref/Domain Classes/ident.adoc | 20 ++-
5 files changed, 322 insertions(+), 5 deletions(-)
diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy
new file mode 100644
index 0000000000..58c2af5c2e
--- /dev/null
+++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy
@@ -0,0 +1,140 @@
+/*
+ * 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 grails.gorm.tests
+
+import groovy.transform.EqualsAndHashCode
+
+import org.hibernate.Hibernate
+
+import grails.gorm.annotation.Entity
+
+class MappedIdentifierSpec extends HibernateGormDatastoreSpec {
+
+ void setupSpec() {
+ manager.registerDomainClasses(MappedIdentOwner, MappedIdentNamedRecord, MappedIdentCompositeRecord)
+ }
+
+ void 'ident returns the generated identifier and null before persistence'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner')
+
+ expect:
+ owner.ident() == null
+
+ when:
+ owner.save(flush: true, failOnError: true)
+
+ then:
+ owner.ident() == owner.id
+ owner.ident() != null
+ }
+
+ void 'ident uses the mapped identifier name'() {
+ given:
+ def record = new MappedIdentNamedRecord(code: 'NAMED-1', payload: 'content')
+
+ expect:
+ record.ident() == 'NAMED-1'
+
+ when:
+ record.save(flush: true, failOnError: true)
+ MappedIdentNamedRecord.withSession { it.clear() }
+
+ then:
+ MappedIdentNamedRecord.get(record.ident()).payload == 'content'
+ }
+
+ void 'ident copies only composite key fields into a separate identifier'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner')
+ def record = new MappedIdentCompositeRecord(owner: owner, code: 'PARAMETER', payload: 'content')
+
+ when:
+ Serializable identifier = record.ident()
+
+ then:
+ identifier instanceof MappedIdentCompositeRecord
+ !identifier.is(record)
+ identifier.owner.is(owner)
+ identifier.code == 'PARAMETER'
+ identifier.payload == null
+ record.id == null
+ }
+
+ void 'a composite identifier containing an association supports get and lazy load'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner').save(failOnError: true)
+ def record = new MappedIdentCompositeRecord(owner: owner, code: 'PARAMETER', payload: 'content')
+ .save(flush: true, failOnError: true)
+ Serializable identifier = record.ident()
+ assert identifier != null
+ MappedIdentCompositeRecord.withSession { it.clear() }
+
+ when:
+ def reloaded = MappedIdentCompositeRecord.get(identifier)
+
+ then:
+ reloaded != null
+ reloaded.code == 'PARAMETER'
+ reloaded.owner.id == owner.id
+ reloaded.payload == 'content'
+
+ when:
+ MappedIdentCompositeRecord.withSession { it.clear() }
+ def proxy = MappedIdentCompositeRecord.load(identifier)
+
+ then:
+ proxy != null
+ !Hibernate.isInitialized(proxy)
+
+ when:
+ String payload = proxy.payload
+
+ then:
+ payload == 'content'
+ Hibernate.isInitialized(proxy)
+ }
+}
+
+@Entity
+class MappedIdentOwner implements Serializable {
+ String name
+}
+
+@Entity
+class MappedIdentNamedRecord implements Serializable {
+ String code
+ String payload
+
+ static mapping = {
+ id name: 'code', generator: 'assigned'
+ }
+}
+
+@Entity
+@EqualsAndHashCode(includes = ['owner', 'code'])
+class MappedIdentCompositeRecord implements Serializable {
+ MappedIdentOwner owner
+ String code
+ String payload
+
+ static mapping = {
+ id composite: ['owner', 'code']
+ }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy
new file mode 100644
index 0000000000..58c2af5c2e
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MappedIdentifierSpec.groovy
@@ -0,0 +1,140 @@
+/*
+ * 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 grails.gorm.tests
+
+import groovy.transform.EqualsAndHashCode
+
+import org.hibernate.Hibernate
+
+import grails.gorm.annotation.Entity
+
+class MappedIdentifierSpec extends HibernateGormDatastoreSpec {
+
+ void setupSpec() {
+ manager.registerDomainClasses(MappedIdentOwner, MappedIdentNamedRecord, MappedIdentCompositeRecord)
+ }
+
+ void 'ident returns the generated identifier and null before persistence'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner')
+
+ expect:
+ owner.ident() == null
+
+ when:
+ owner.save(flush: true, failOnError: true)
+
+ then:
+ owner.ident() == owner.id
+ owner.ident() != null
+ }
+
+ void 'ident uses the mapped identifier name'() {
+ given:
+ def record = new MappedIdentNamedRecord(code: 'NAMED-1', payload: 'content')
+
+ expect:
+ record.ident() == 'NAMED-1'
+
+ when:
+ record.save(flush: true, failOnError: true)
+ MappedIdentNamedRecord.withSession { it.clear() }
+
+ then:
+ MappedIdentNamedRecord.get(record.ident()).payload == 'content'
+ }
+
+ void 'ident copies only composite key fields into a separate identifier'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner')
+ def record = new MappedIdentCompositeRecord(owner: owner, code: 'PARAMETER', payload: 'content')
+
+ when:
+ Serializable identifier = record.ident()
+
+ then:
+ identifier instanceof MappedIdentCompositeRecord
+ !identifier.is(record)
+ identifier.owner.is(owner)
+ identifier.code == 'PARAMETER'
+ identifier.payload == null
+ record.id == null
+ }
+
+ void 'a composite identifier containing an association supports get and lazy load'() {
+ given:
+ def owner = new MappedIdentOwner(name: 'owner').save(failOnError: true)
+ def record = new MappedIdentCompositeRecord(owner: owner, code: 'PARAMETER', payload: 'content')
+ .save(flush: true, failOnError: true)
+ Serializable identifier = record.ident()
+ assert identifier != null
+ MappedIdentCompositeRecord.withSession { it.clear() }
+
+ when:
+ def reloaded = MappedIdentCompositeRecord.get(identifier)
+
+ then:
+ reloaded != null
+ reloaded.code == 'PARAMETER'
+ reloaded.owner.id == owner.id
+ reloaded.payload == 'content'
+
+ when:
+ MappedIdentCompositeRecord.withSession { it.clear() }
+ def proxy = MappedIdentCompositeRecord.load(identifier)
+
+ then:
+ proxy != null
+ !Hibernate.isInitialized(proxy)
+
+ when:
+ String payload = proxy.payload
+
+ then:
+ payload == 'content'
+ Hibernate.isInitialized(proxy)
+ }
+}
+
+@Entity
+class MappedIdentOwner implements Serializable {
+ String name
+}
+
+@Entity
+class MappedIdentNamedRecord implements Serializable {
+ String code
+ String payload
+
+ static mapping = {
+ id name: 'code', generator: 'assigned'
+ }
+}
+
+@Entity
+@EqualsAndHashCode(includes = ['owner', 'code'])
+class MappedIdentCompositeRecord implements Serializable {
+ MappedIdentOwner owner
+ String code
+ String payload
+
+ static mapping = {
+ id composite: ['owner', 'code']
+ }
+}
diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
index 9ae2bfa6aa..a05d713683 100644
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormInstanceApi.groovy
@@ -35,7 +35,10 @@ import org.grails.datastore.mapping.core.connections.ConnectionSources
import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
import org.grails.datastore.mapping.proxy.EntityProxy
+import org.grails.datastore.mapping.reflect.EntityReflector
import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
import org.grails.datastore.mapping.validation.ValidationException
@@ -273,7 +276,24 @@ class GormInstanceApi<D> extends AbstractGormApi<D> implements GormInstanceOpera
@Override
Serializable ident(D instance) {
- (Serializable)InvokerHelper.getProperty(instance, 'id')
+ PersistentEntity entity = mappingContext.getPersistentEntity(persistentClass.name)
+ PersistentProperty identity = entity.identity
+ if (identity != null) {
+ return (Serializable) InvokerHelper.getProperty(instance, identity.name)
+ }
+
+ PersistentProperty[] idProperties = entity.compositeIdentity
+ if (idProperties != null) {
+ def identifier = entity.newInstance()
+ if (identifier instanceof Serializable) {
+ EntityReflector reflector = entity.reflector
+ for (PersistentProperty property : idProperties) {
+ reflector.setProperty(identifier, property.name, reflector.getProperty(instance, property.name))
+ }
+ return (Serializable) identifier
+ }
+ }
+ return null
}
@Override
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormInstanceApiSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormInstanceApiSpec.groovy
index 7b156ac846..ba9f0f5f10 100644
--- a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormInstanceApiSpec.groovy
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/GormInstanceApiSpec.groovy
@@ -557,13 +557,12 @@ class GormInstanceApiSpec extends Specification {
notThrown(Exception)
}
- void "ident reads the instance's id property"() {
+ void "ident returns the default mapped identifier through the domain API"() {
given:
- def api = new GormInstanceApi(GormInstanceApiThing, datastore)
def saved = new GormInstanceApiThing(name: 'a').save(flush: true)
expect:
- api.ident(saved) == saved.id
+ saved.ident() == saved.id
}
void "isAttached reports true for an instance persisted within the current session"() {
diff --git a/grails-doc/src/en/ref/Domain Classes/ident.adoc b/grails-doc/src/en/ref/Domain Classes/ident.adoc
index e04ab8875a..92cbf83bad 100644
--- a/grails-doc/src/en/ref/Domain Classes/ident.adoc
+++ b/grails-doc/src/en/ref/Domain Classes/ident.adoc
@@ -25,7 +25,9 @@ under the License.
=== Purpose
-Returns the value of the identity property of the domain class regardless of the name of the identity property itself
+Returns the value of the mapped identity property of the domain class, regardless of its name. For example, when the mapping uses `id name: 'code'`, `ident()` returns the value of `code`. A generated identifier remains `null` until it has been assigned.
+
+For a composite identity, `ident()` returns a separate serializable instance of the domain class with the mapped key properties copied into it, including any associations that form part of the key. Other properties retain their new-instance defaults. Pass the returned identifier to `get()` or `load()` to retrieve the entity.
=== Examples
@@ -38,3 +40,19 @@ b.save()
println b.ident()
----
+
+For a `BookEdition` mapped with `id composite: ['isbn', 'edition']`:
+
+[source,groovy]
+----
+def bookEdition = new BookEdition(isbn: '9780307743657', edition: 1, title: 'The Shining')
+bookEdition.save(flush: true, failOnError: true)
+
+def identifier = bookEdition.ident()
+assert identifier.isbn == bookEdition.isbn
+assert identifier.edition == bookEdition.edition
+assert !identifier.is(bookEdition)
+
+def reloaded = BookEdition.get(identifier)
+def reference = BookEdition.load(identifier)
+----