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:25 UTC

(grails-core) 08/29: feat: describe table-per-concrete-class hierarchies 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 99bd0e810a4648cb2f0c7d725d7a1a66a267aacf
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Mon Oct 5 00:08:01 2026 -0500

    feat: describe table-per-concrete-class hierarchies in GrailsDomainGenerator
    
    A table-per-class root states @Inheritance(TABLE_PER_CLASS) and each subclass its own
    table. The generated class is abstract exactly when the entity is, which is how
    Hibernate's annotation binder learns that an abstract root's table is abstract; the
    facets report the binder's own isTableAbstract for roots. Specs read abstract and
    three-level hierarchies back through Hibernate's annotation binder.
    
    Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
 .../domainbinding/jpa/GrailsDomainGenerator.groovy |   3 -
 .../jpa/GrailsDomainGeneratorSpec.groovy           | 142 ++++++++++++++++++++-
 2 files changed, 138 insertions(+), 7 deletions(-)

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 c226b56a05..d08e6a4a7d 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
@@ -318,9 +318,6 @@ class GrailsDomainGenerator {
                     "Entity [${entity.name}] uses ${strategy} but the root of its hierarchy uses ${hierarchyStrategy}: " +
                             'a hierarchy that mixes inheritance strategies is not supported')
         }
-        if (strategy == InheritanceType.TABLE_PER_CLASS) {
-            throw new UnsupportedOperationException("Inheritance strategy ${strategy} of [${entity.name}] is not supported yet")
-        }
         boolean singleTable = strategy == InheritanceType.SINGLE_TABLE
         String discriminatorValue = null
         if (singleTable) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorSpec.groovy
index 2f1635476a..faaa537151 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorSpec.groovy
@@ -64,6 +64,7 @@ import org.hibernate.mapping.PersistentClass
 import org.hibernate.mapping.JoinedSubclass
 import org.hibernate.mapping.RootClass
 import org.hibernate.mapping.SingleTableSubclass
+import org.hibernate.mapping.UnionSubclass
 import org.hibernate.mapping.GeneratorCreator
 import org.hibernate.boot.registry.BootstrapServiceRegistry
 import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
@@ -110,7 +111,8 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
         manager.registerDomainClasses(GenBasic, GenVehicle, GenCar, GenWithEnum, GenWithOwner, GenDerived, GenTyped, GenUnsupportedType, GenIdSequence, GenIdUuid, GenIdAssigned, GenIdTable,
                 GenIdIncrement, GenIdIdentity, GenIdNative, GenAnimal, GenDog, GenPuppy, GenCat, GenToy, GenPlushToy, GenGadget,
                 GenGizmo, GenCoded, GenCodedChild, GenFormulaRoot, GenFormulaChild, GenAbstractBase, GenConcreteChild, GenNoted, GenNotedChild,
-                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild)
+                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild,
+                GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan, GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom)
     }
 
     List<StandardServiceRegistry> registries = []
@@ -551,7 +553,8 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
     void "an abstract class in a hierarchy is generated abstract"() {
         when:
         Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAbstractBase, GenConcreteChild, GenNoted, GenNotedChild,
-                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild)
+                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild,
+                GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan, GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom)
 
         then:
         java.lang.reflect.Modifier.isAbstract(classes[entity(GenAbstractBase)].modifiers)
@@ -562,7 +565,8 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
     void "a single-table subclass states its own class-level facets but never a table or a comment"() {
         when:
         Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenNoted, GenNotedChild,
-                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild)
+                GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild,
+                GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan, GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom)
         Class<?> root = classes[entity(GenNoted)]
         Class<?> child = classes[entity(GenNotedChild)]
 
@@ -609,7 +613,8 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
 
     void "a joined subclass keys its table with the name of the identifier column and keeps an explicit table name"() {
         when:
-        Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenJoinedKeyed, GenJoinedKeyedChild)
+        Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenJoinedKeyed, GenJoinedKeyedChild,
+                GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan, GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom)
 
         then:
         classes[entity(GenJoinedKeyed)].getAnnotation(Table).name() == 'keyed_roots'
@@ -645,6 +650,74 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
         bound.superclass.entityName == GenJoinedCar.name
     }
 
+    void "a table-per-concrete-class hierarchy states the strategy on the root and a table on every subclass only"() {
+        when:
+        Map<GrailsHibernatePersistentEntity, Class<?>> classes =
+                generateHierarchy(GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan)
+        Class<?> root = classes[entity(GenFleetVehicle)]
+        Class<?> car = classes[entity(GenFleetCar)]
+        Class<?> sports = classes[entity(GenFleetSportsCar)]
+
+        then:
+        root.getAnnotation(Inheritance).strategy() == InheritanceType.TABLE_PER_CLASS
+        root.getAnnotation(Table).name() == 'gen_fleet_vehicle'
+        car.getAnnotation(Table).name() == 'gen_fleet_car'
+        sports.getAnnotation(Table).name() == 'gen_fleet_sports_car'
+        !car.isAnnotationPresent(Inheritance)
+        [root, car, sports].every {
+            !it.isAnnotationPresent(PrimaryKeyJoinColumn) && !it.isAnnotationPresent(DiscriminatorColumn) &&
+                    !it.isAnnotationPresent(DiscriminatorValue)
+        }
+        sports.superclass == car
+        classes[entity(GenFleetSedan)].superclass == car
+        car.superclass == root
+        !java.lang.reflect.Modifier.isAbstract(root.modifiers)
+    }
+
+    void "the facets of an abstract table-per-concrete-class root say its table is abstract"() {
+        when:
+        HierarchyFacets root = newGenerator().hierarchyFacets(entity(GenUnionBase))
+        HierarchyFacets middle = newGenerator().hierarchyFacets(entity(GenUnionMiddle))
+        HierarchyFacets leaf = newGenerator().hierarchyFacets(entity(GenUnionLeaf))
+
+        then:
+        root.strategy() == InheritanceType.TABLE_PER_CLASS
+        root.abstractClass() && root.abstractTable()
+        middle.abstractClass() && middle.abstractTable()
+        !leaf.abstractClass() && !leaf.abstractTable()
+        leaf.ownsTable() && leaf.superclass() == GenUnionBase.name
+    }
+
+    void "Hibernate's annotation binder reads an abstract table-per-concrete-class root as the binder builds it"() {
+        given:
+        Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom)
+
+        when:
+        Metadata metadata = annotationMetadata(classes.values())
+        PersistentClass root = metadata.getEntityBinding(classes[entity(GenUnionBase)].name)
+        PersistentClass leaf = metadata.getEntityBinding(classes[entity(GenUnionLeaf)].name)
+        PersistentClass bottom = metadata.getEntityBinding(classes[entity(GenUnionBottom)].name)
+        PersistentClass boundRoot = getPersistentEntity(GenUnionBase).persistentClass
+        PersistentClass boundBottom = getPersistentEntity(GenUnionBottom).persistentClass
+
+        then:
+        root instanceof RootClass
+        leaf instanceof UnionSubclass
+        bottom instanceof UnionSubclass
+        bottom.superclass.entityName == classes[entity(GenUnionMiddle)].name
+        bottom.table.name == 'gen_union_bottom'
+        root.isAbstract() == true
+        root.table.isAbstractUnionTable()
+
+        and: "the binder built the same shape"
+        boundBottom instanceof UnionSubclass
+        boundBottom.table.name == bottom.table.name
+        boundRoot.isAbstract() == root.isAbstract()
+        boundRoot.table.isAbstractUnionTable() == root.table.isAbstractUnionTable()
+        getPersistentEntity(GenUnionMiddle).persistentClass.table.isAbstractUnionTable() ==
+                metadata.getEntityBinding(classes[entity(GenUnionMiddle)].name).table.isAbstractUnionTable()
+    }
+
     void "a hierarchy must be generated whole"() {
         when:
         newGenerator().generateAll(entityClasses.collect { entity(it) }, getClass().classLoader)
@@ -678,6 +751,8 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
         GenBasic    | null                         | null           | true      | null          | false
         GenJoinedVehicle | InheritanceType.JOINED  | null           | true      | null          | false
         GenJoinedCar     | InheritanceType.JOINED  | GenJoinedVehicle.name | true | null     | false
+        GenFleetVehicle  | InheritanceType.TABLE_PER_CLASS | null   | true      | null          | false
+        GenFleetCar      | InheritanceType.TABLE_PER_CLASS | GenFleetVehicle.name | true | null | false
     }
 
     void "Hibernate's annotation binder reads the generated single-table hierarchy as the binder builds it"() {
@@ -998,6 +1073,65 @@ class GenJoinedKeyedChild extends GenJoinedKeyed {
     }
 }
 
+@Entity
+class GenFleetVehicle {
+
+    String name
+
+    static mapping = {
+        tablePerHierarchy false
+        tablePerConcreteClass true
+        id generator: 'table'
+    }
+}
+
+@Entity
+class GenFleetCar extends GenFleetVehicle {
+
+    Integer doors
+}
+
+@Entity
+class GenFleetSportsCar extends GenFleetCar {
+
+    Integer topSpeed
+}
+
+@Entity
+class GenFleetSedan extends GenFleetCar {
+
+    Boolean limousine
+}
+
+@Entity
+abstract class GenUnionBase {
+
+    String title
+
+    static mapping = {
+        tablePerConcreteClass true
+        id generator: 'table'
+    }
+}
+
+@Entity
+class GenUnionLeaf extends GenUnionBase {
+
+    String leafValue
+}
+
+@Entity
+abstract class GenUnionMiddle extends GenUnionBase {
+
+    String middleValue
+}
+
+@Entity
+class GenUnionBottom extends GenUnionMiddle {
+
+    String bottomValue
+}
+
 @Entity
 abstract class GenAbstractBase {