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/10/05 12:55:40 UTC

(grails-core) 23/29: feat: state lazy: true on simple properties in GrailsDomainGenerator

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 9.0.x-h7-improvement-phase3
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 65247fed037656eb334fd3e75ab2c20b7005095e
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Mon Oct 5 04:39:16 2026 -0500

    feat: state lazy: true on simple properties in GrailsDomainGenerator
    
    PropertyBinder marks a Hibernate property lazy whenever the mapping says lazy: true
    (its accessor is not limited to associations), so a simple or enum property mapped
    lazy was a lazy attribute in the binder and an eager one in a generated model.
    
    The generated field now carries @Basic(fetch = LAZY) in that case. An embedded
    property mapped lazy is rejected by name, because an @Embedded cannot state it. The
    differential now compares Property.lazy of every leaf and to-one association with
    what Hibernate's annotation binder builds.
    
    Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
 .../domainbinding/jpa/GrailsDomainGenerator.groovy |   9 ++
 .../GrailsDomainGeneratorDifferentialSpec.groovy   |   4 +
 .../GrailsDomainGeneratorLazyPropertySpec.groovy   | 107 +++++++++++++++++++++
 3 files changed, 120 insertions(+)

diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
index b4a855856a..b3748c0b96 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
@@ -27,6 +27,7 @@ import jakarta.persistence.AssociationOverride
 import jakarta.persistence.AssociationOverrides
 import jakarta.persistence.AttributeOverride
 import jakarta.persistence.AttributeOverrides
+import jakarta.persistence.Basic
 import jakarta.persistence.Cacheable
 import jakarta.persistence.CascadeType
 import jakarta.persistence.CollectionTable
@@ -1608,6 +1609,10 @@ class GrailsDomainGenerator {
         if (!type.isRoot()) {
             return "the embedded type [${type.name}] extends another persistent class"
         }
+        if (property.isLazy()) {
+            return 'the property is mapped lazy: true, which the binder marks on the component as a lazy attribute and which ' +
+                    'annotations cannot state on an @Embedded'
+        }
         if (visiting.contains(type.javaClass)) {
             return "the embedded type [${type.name}] contains itself"
         }
@@ -1861,6 +1866,10 @@ class GrailsDomainGenerator {
         if (naturalMutable != null) {
             annotations << AnnotationDescription.Builder.ofType(HibernateNaturalId).define('mutable', naturalMutable).build()
         }
+        if (!(property instanceof HibernateSimpleIdentityProperty) && property.isLazy()) {
+            // PropertyBinder marks the property lazy when the mapping says lazy: true (the version never is)
+            annotations << AnnotationDescription.Builder.ofType(Basic).define('fetch', FetchType.LAZY).build()
+        }
         // an identifier's type is decided with its generator, which the generator does not describe yet
         TypeFacets type = property instanceof HibernateSimpleIdentityProperty ? null : typeFacets(property)
         if (type != null) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorDifferentialSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorDifferentialSpec.groovy
index 8a3ca61215..436dba0edc 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorDifferentialSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorDifferentialSpec.groovy
@@ -1360,6 +1360,7 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
                 cascade       : [cascadeActions(boundProperty.cascade), cascadeActions(annotatedProperty.cascade)],
                 insertable    : [boundProperty.insertable, annotatedProperty.insertable],
                 updatable     : [boundProperty.updateable, annotatedProperty.updateable],
+                propertyLazy  : [boundProperty.lazy, annotatedProperty.lazy],
         ]
         if (bound instanceof OneToOne) {
             pairs.referencedProperty = [bound.referencedPropertyName, annotated.referencedPropertyName]
@@ -1641,6 +1642,9 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
             return ["${where} columns: generator=${annotatedSelectables}${annotatedNullable} binder=${boundSelectables}${boundNullable}".toString()]
         }
         List<String> found = []
+        if (bound.lazy != annotated.lazy) {
+            found << "${where} propertyLazy: generator=${annotated.lazy} binder=${bound.lazy}".toString()
+        }
         for (int i = 0; i < boundColumns.size(); i++) {
             Column boundColumn = boundColumns[i]
             Column annotatedColumn = annotatedColumns[i]
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorLazyPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorLazyPropertySpec.groovy
new file mode 100644
index 0000000000..260c17e0fd
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorLazyPropertySpec.groovy
@@ -0,0 +1,107 @@
+/*
+ *  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 org.grails.orm.hibernate.cfg.domainbinding.jpa
+
+import grails.gorm.annotation.Entity
+import jakarta.persistence.Basic
+import jakarta.persistence.FetchType
+import org.hibernate.boot.Metadata
+import org.hibernate.mapping.PersistentClass
+
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
+
+/**
+ * Describes how {@link GrailsDomainGenerator} states {@code lazy: true} on a simple or enum property: the binder marks
+ * the Hibernate property lazy (a lazy attribute, which Hibernate loads on first access when the class is enhanced), and
+ * the annotation for that is {@code @Basic(fetch = LAZY)}. Where the binder sets it and annotations cannot say it, the
+ * generator rejects the entity by name.
+ */
+class GrailsDomainGeneratorLazyPropertySpec extends GrailsDomainGeneratorSupport {
+
+    void setupSpec() {
+        manager.registerDomainClasses(GenLazyBasic, GenLazyEmbedded)
+    }
+
+    void "a lazy property is a basic attribute fetched lazily"() {
+        when:
+        Class<?> generated = generateGroup(GenLazyBasic).values().first()
+
+        then:
+        generated.getDeclaredField('big').getAnnotation(Basic).fetch() == FetchType.LAZY
+        generated.getDeclaredField('state').getAnnotation(Basic).fetch() == FetchType.LAZY
+        !generated.getDeclaredField('plain').isAnnotationPresent(Basic)
+    }
+
+    void "Hibernate's annotation binder reads the lazy properties as the binder bound them"() {
+        given:
+        Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateGroup(GenLazyBasic)
+        Metadata metadata = annotationMetadata(classes.values())
+        PersistentClass bound = entity(GenLazyBasic).persistentClass
+        PersistentClass read = metadata.getEntityBinding(classes[entity(GenLazyBasic)].name)
+
+        expect:
+        ['plain', 'big', 'state'].every { String name -> bound.getProperty(name).lazy == read.getProperty(name).lazy }
+        bound.getProperty('big').lazy
+    }
+
+    void "lazy on an embedded property is rejected by name"() {
+        when:
+        generateGroup(GenLazyEmbedded)
+
+        then:
+        UnsupportedOperationException e = thrown()
+        e.message.contains('GenLazyEmbedded')
+        e.message.contains('home')
+        e.message.contains('lazy')
+    }
+}
+
+@Entity
+class GenLazyBasic {
+
+    String plain
+    String big
+    GenLazyState state
+
+    static mapping = {
+        big lazy: true
+        state lazy: true
+    }
+}
+
+@Entity
+class GenLazyEmbedded {
+
+    GenLazyHome home
+
+    static embedded = ['home']
+
+    static mapping = {
+        home lazy: true
+    }
+}
+
+class GenLazyHome {
+
+    String street
+}
+
+enum GenLazyState {
+    ON, OFF
+}