jdaugherty commented on code in PR #16653:
URL: https://github.com/apache/grails-core/pull/16653#discussion_r4294836385


##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy:
##########
@@ -633,6 +633,23 @@ class AstUtils {
         return null
     }
 
+    /**
+     * Finds the highest uncompiled domain class in the contiguous domain 
hierarchy.
+     * Plain superclasses may be shared with non-persistent classes and must 
not receive
+     * generated persistence properties.
+     *
+     * @param classNode the domain class being enhanced
+     * @return the domain class that should declare generated persistence 
properties
+     */
+    static ClassNode getFurthestUnresolvedDomainParent(ClassNode classNode) {
+        ClassNode parent = classNode.getSuperClass()
+        while (parent != null && !parent.isResolved() && 
isDomainClass(parent)) {

Review Comment:
   This stops at the first plain parent, so a plain class *between* two domain 
classes breaks the one-identity rule and brings GRAILS-3209 back, depending on 
declaration order.
   
   Probe: `@Entity class Dog extends Mammal`, `abstract class Mammal extends 
Animal` (plain), `@Entity abstract class Animal`. With Dog declared first, Dog 
and Animal both get `id`/`version` (8.0.x head: Animal only, in both orders). 
`isDomainClass(Mammal)` is false, so Dog receives the fields; Animal then 
inherits none and receives its own.
   
   Could you return the furthest unresolved *domain* ancestor, walking past 
plain intermediates, and add that case in both orders to the `where:` tables 
here, in `GormEntityTransformSpec` and in `DefaultDomainClassInjectorSpec`? 
Something like:
   
   ```groovy
   static ClassNode getFurthestUnresolvedDomainParent(ClassNode classNode) {
       ClassNode target = classNode
       ClassNode parent = classNode.getSuperClass()
       while (parent != null && !parent.isResolved() && parent.name != 
'java.lang.Object') {
           if (isDomainClass(parent)) {
               target = parent
           }
           parent = parent.getSuperClass()
       }
       return target
   }
   ```
   
   (with the uncached check from the other thread in place of `isDomainClass`). 
With both, the probe matches the current behaviour and the new specs stay green.



##########
grails-datamapping-core/src/test/groovy/org/grails/compiler/gorm/GormEntityTransformSpec.groovy:
##########
@@ -36,6 +36,67 @@ import 
org.grails.datastore.mapping.dirty.checking.DirtyCheckable
  */
 class GormEntityTransformSpec extends Specification{
 
+    void 'generated fields do not leak into a shared non-domain superclass'() {
+        given:
+        def loader = new GroovyClassLoader()
+        loader.parseClass("""
+            import grails.gorm.annotation.Entity
+
+            $declarations
+
+            abstract class SharedDetails {
+                String name
+            }
+
+            class PlainCommand extends SharedDetails { }
+        """)
+
+        when:
+        def root = loader.loadClass('RootEntity')
+        def child = loader.loadClass('ChildEntity')
+        def command = 
loader.loadClass('PlainCommand').getDeclaredConstructor().newInstance()
+
+        then:
+        root.getDeclaredField('id').type == Long
+        root.getDeclaredField('version').type == Long
+        child.getMethod('getId').declaringClass == root
+        child.getMethod('getVersion').declaringClass == root
+        !child.declaredFields*.name.contains('id')
+        !child.declaredFields*.name.contains('version')
+        command.hasProperty('id') == null
+        command.hasProperty('version') == null
+
+        cleanup:
+        loader.close()
+
+        where:
+        declarations << [

Review Comment:
   Please add rows for a plain class between two domain classes, in both 
declaration orders (the `AstUtils` thread has the shape). That is the case that 
currently regresses, and the same rows belong in 
`DefaultDomainClassInjectorSpec`.



##########
grails-doc/src/en/guide/theWebLayer/controllers/commandObjects.adoc:
##########
@@ -59,6 +59,39 @@ class AuthorSearchCommand implements 
grails.validation.Validateable {
 In this example, both `name` and `age` will allow null values during 
validation.
 
 
+==== Sharing Properties With Domain Classes

Review Comment:
   Could this section lead with a trait? That is the idiomatic way to share 
properties between a command object and a domain class, and it works without 
this change: `trait PersonDetails { String name }` implemented by both `Person` 
and `PersonCommand` maps `name` on the entity, and the command validates `name` 
alone with no `id`/`version`. Then describe the shared superclass as what 
Grails does when one is used.
   
   The GORM inheritance chapter 
(`grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc`,
 and the hibernate5 copy) only covers abstract *domain* roots today. A sentence 
there on where generated `id`/`version` land when the parent is a plain class, 
and that `@DirtyCheck` on a plain base opts it into dirty checking but not into 
field placement, would close the loop.



##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstUtils.groovy:
##########
@@ -633,6 +633,23 @@ class AstUtils {
         return null
     }
 
+    /**
+     * Finds the highest uncompiled domain class in the contiguous domain 
hierarchy.
+     * Plain superclasses may be shared with non-persistent classes and must 
not receive
+     * generated persistence properties.
+     *
+     * @param classNode the domain class being enhanced
+     * @return the domain class that should declare generated persistence 
properties
+     */
+    static ClassNode getFurthestUnresolvedDomainParent(ClassNode classNode) {

Review Comment:
   `isDomainClass(ClassNode)` is `@Memoized`, and `ClassNode` equality is by 
name, so where the fields land now depends on what the compiling JVM has 
already seen for that name. Gradle keeps compiler-daemon statics across 
compilations, and test JVMs compile many same-named fixtures.
   
   Probe in one JVM: compile `abstract class ParentName { String name }` + 
`@Entity class X extends ParentName` in one `GroovyClassLoader`, then `@Entity 
class Kid extends ParentName; @Entity abstract class ParentName { }` in 
another. `ParentName` and `Kid` both get `id`/`version`: the cached `false` 
wins over the annotation on the new node (8.0.x head: ParentName only).
   
   `DirtyCheckingTransformer` has the same exposure, but a stale answer there 
costs a missed weave; here it costs a second identity. Could placement use an 
uncached check (entity annotations, `GormEntity` interface, module path), for 
example an uncached variant in `AstUtils` that this helper and 
`DirtyCheckingTransformer` share?



##########
grails-doc/src/en/guide/theWebLayer/controllers/commandObjects.adoc:
##########
@@ -59,6 +59,39 @@ class AuthorSearchCommand implements 
grails.validation.Validateable {
 In this example, both `name` and `age` will allow null values during 
validation.
 
 
+==== Sharing Properties With Domain Classes
+
+A command object and a domain class can extend the same non-domain superclass 
to share properties.
+Keep that superclass in `src/main/groovy`, outside `grails-app/domain`, and do 
not annotate it as an entity.
+
+[source,groovy]
+----
+abstract class PersonDetails {
+    String name
+}
+
[email protected]
+class Person extends PersonDetails {
+    static constraints = {
+        name blank: false
+    }
+}
+
+class PersonCommand extends PersonDetails implements 
grails.validation.Validateable {
+    static constraints = {
+        name blank: false
+    }
+}
+----
+
+Grails adds the generated `id` and `version` properties to the domain class, 
not to the shared non-domain superclass.
+Consequently, `PersonCommand` validates `name` without acquiring persistence 
properties from `Person`.
+When domain classes extend other domain classes, generated persistence 
properties remain on the domain superclass.
+Properties explicitly declared on a superclass are still inherited normally.
+
+After upgrading an application affected by persistence properties being added 
to a shared non-domain superclass, run a clean build to regenerate the affected 
classes.

Review Comment:
   This belongs in the upgrade notes rather than here, and it is more than a 
clean build. The change is visible to any plain base class compiled alongside 
its domain subclass:
   
   - `@CompileStatic abstract class Base { boolean isNew() { id == null } }` 
with `@Entity class Foo extends Base` compiles on 8.0.x and fails with this 
change: `[Static type checking] - The variable [id] is undeclared.` (a dynamic 
base keeps working at runtime).
   - `@ToString`, `@EqualsAndHashCode` or a hand-written `equals` on the base 
no longer see `id`/`version`.
   - A stale `.class` of the base keeps the fields until a clean build.
   
   Please add a numbered section to `upgrading90x.adoc` covering these and keep 
this chapter to the behaviour.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to