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 {