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/06 13:06:45 UTC
(grails-core) 03/07: fix: add no not-null check to the table of a generated single-table hierarchy
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 fb20ef423ad2272d51856a79582909f0c58aed54
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Mon Oct 5 23:02:15 2026 -0500
fix: add no not-null check to the table of a generated single-table hierarchy
Hibernate treats a primitive field as not optional and, in a single-table hierarchy, adds class <> 'X' or (column is not null) to the
table of each subclass for every such property whose column is nullable (the column facets match the binder, which keeps it nullable).
The domain binder adds no check, so the carrier field of a primitive property of a single-table hierarchy is boxed.
Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
.../domainbinding/jpa/GrailsDomainGenerator.groovy | 22 ++++++++++-
.../generated/GeneratedDomainClassesDdlSpec.groovy | 13 +++++-
...eneratedDomainClassesDdlDifferentialSpec.groovy | 1 -
.../jpa/GrailsDomainGeneratorSpec.groovy | 46 +++++++++++++++++++++-
4 files changed, 77 insertions(+), 5 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 2b1073c1c2..0bc6c22726 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
@@ -108,6 +108,8 @@ import org.hibernate.type.BasicType
import org.hibernate.type.spi.TypeConfiguration
import org.hibernate.usertype.UserType
+import org.springframework.util.ClassUtils
+
import org.grails.datastore.mapping.model.config.GormProperties
import org.grails.datastore.mapping.model.types.Association
import org.grails.orm.hibernate.cfg.CacheConfig
@@ -2325,10 +2327,28 @@ class GrailsDomainGenerator {
for (Annotation constraint : validationAnnotations(property)) {
annotations << AnnotationDescription.ForLoadedAnnotation.of(constraint)
}
- return builder.defineField(property.name, property.type, Visibility.PRIVATE)
+ return builder.defineField(property.name, columnFieldType(property), Visibility.PRIVATE)
.annotateField(annotations as AnnotationDescription[])
}
+ /**
+ * The type of the field of a basic property. Hibernate treats a primitive field as not optional and, for every property of a
+ * single-table hierarchy with subclasses (the properties the root declares as well as those of the subclasses), adds a table
+ * check constraint ({@code class <> 'X' or (column is not null)}) to a subclass when the column is nullable; the domain binder
+ * adds none, and the column facets already say whether the column is nullable. The carrier is never instantiated, so such a
+ * field is boxed.
+ */
+ private static Class<?> columnFieldType(HibernatePersistentProperty property) {
+ Class<?> type = property.type
+ if (type.primitive && property.owner instanceof GrailsHibernatePersistentEntity) {
+ GrailsHibernatePersistentEntity owner = (GrailsHibernatePersistentEntity) property.owner
+ if (inheritanceType(owner) == InheritanceType.SINGLE_TABLE && (!owner.isRoot() || !owner.childEntities.isEmpty())) {
+ return ClassUtils.resolvePrimitiveIfNecessary(type)
+ }
+ }
+ return type
+ }
+
/**
* A many-to-one association is a field typed with the class generated for its target (which is why the target must be
* part of the same call), with {@code @ManyToOne} stating the fetch type, the optionality and the cascade, a
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/generated/GeneratedDomainClassesDdlSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/generated/GeneratedDomainClassesDdlSpec.groovy
index 3f81e66d75..05bf968599 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/generated/GeneratedDomainClassesDdlSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/generated/GeneratedDomainClassesDdlSpec.groovy
@@ -65,6 +65,13 @@ class GeneratedDomainClassesDdlSpec extends HibernateGormDatastoreSpec {
checkClauses('gdd_vehicle').isEmpty()
}
+ void "a primitive property of a single-table subclass adds no not-null check to the table, as with the domain binder"() {
+ expect:
+ checkClauses('gdd_vehicle').isEmpty()
+ datastore.metadata.getEntityBinding(GddCar.name).getProperty('doors').columns[0].nullable
+ datastore.metadata.getEntityBinding(GddVehicle.name).getProperty('wheels').columns[0].nullable
+ }
+
void "the discriminator of the root is not forced, so root queries do not filter on it"() {
given:
RootClass root = (RootClass) datastore.metadata.getEntityBinding(GddVehicle.name)
@@ -76,7 +83,7 @@ class GeneratedDomainClassesDdlSpec extends HibernateGormDatastoreSpec {
void "the instances of the hierarchy still save, load polymorphically and keep their discriminator"() {
when:
- new GddCar(name: 'c', doors: 4).save(flush: true)
+ new GddCar(name: 'c', doors: 4, sporty: true).save(flush: true)
new GddTruck(name: 't', axles: 3).save(flush: true)
session.clear()
@@ -89,11 +96,13 @@ class GeneratedDomainClassesDdlSpec extends HibernateGormDatastoreSpec {
@Entity
class GddVehicle {
String name
+ int wheels
}
@Entity
class GddCar extends GddVehicle {
- Integer doors
+ int doors
+ boolean sporty
}
@Entity
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GeneratedDomainClassesDdlDifferentialSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GeneratedDomainClassesDdlDifferentialSpec.groovy
index 6de5222d6d..b9b54d22b1 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GeneratedDomainClassesDdlDifferentialSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GeneratedDomainClassesDdlDifferentialSpec.groovy
@@ -65,7 +65,6 @@ class GeneratedDomainClassesDdlDifferentialSpec extends Specification {
*/
private static final List<Map> KNOWN = [
[id: 'LIST_INDEX_CHECK', reason: 'not yet analysed'],
- [id: 'SUBCLASS_NOT_NULL_CHECK', reason: 'not yet analysed'],
[id: 'ENUM_COLUMN_EXTRAS', reason: 'not yet analysed'],
[id: 'COLLECTION_TABLE_KEY', reason: 'not yet analysed'],
[id: 'CIRCULAR_MANY_TO_MANY', reason: 'not yet analysed'],
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 309998eb75..7f54908f23 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
@@ -145,7 +145,7 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
void setupSpec() {
manager.registerDomainClasses(GenBasic, GenVehicle, GenCar, GenWithEnum, GenWithOwner, GenDerived, GenTyped, GenUnsupportedType, GenIdSequence, GenIdUuid, GenIdAssigned, GenIdTable,
- GenIdIncrement, GenIdIdentity, GenIdNative, GenAnimal, GenDog, GenPuppy, GenCat, GenToy, GenPlushToy, GenGadget,
+ GenIdIncrement, GenIdIdentity, GenIdNative, GenPrimVehicle, GenPrimCar, GenPrimJoinedRoot, GenPrimJoinedChild, GenAnimal, GenDog, GenPuppy, GenCat, GenToy, GenPlushToy, GenGadget,
GenGizmo, GenCoded, GenCodedChild, GenFormulaRoot, GenFormulaChild, GenAbstractBase, GenConcreteChild, GenNoted, GenNotedChild,
GenJoinedVehicle, GenJoinedCar, GenJoinedSportsCar, GenJoinedSedan, GenJoinedKeyed, GenJoinedKeyedChild,
GenFleetVehicle, GenFleetCar, GenFleetSportsCar, GenFleetSedan, GenUnionBase, GenUnionLeaf, GenUnionMiddle, GenUnionBottom,
@@ -592,6 +592,21 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
classes[entity(GenGizmo)].getAnnotation(DiscriminatorValue).value() == GenGizmo.name
}
+ void "a primitive property of a single-table subclass is a boxed field, so Hibernate adds no not-null check for the subclass"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> single = generateHierarchy(GenPrimVehicle, GenPrimCar)
+ Map<GrailsHibernatePersistentEntity, Class<?>> joined = generateHierarchy(GenPrimJoinedRoot, GenPrimJoinedChild)
+
+ then: 'a joined root and subclass keep the primitive, which makes the column not null in their own table'
+ joined[entity(GenPrimJoinedChild)].getDeclaredField('doors').type == int
+ joined[entity(GenPrimJoinedRoot)].getDeclaredField('name').type == String
+
+ and: 'the root and the subclasses of a single-table hierarchy share the table, where such a column must stay nullable'
+ single[entity(GenPrimVehicle)].getDeclaredField('wheels').type == Integer
+ single[entity(GenPrimCar)].getDeclaredField('doors').type == Integer
+ single[entity(GenPrimCar)].getDeclaredField('sporty').type == Boolean
+ }
+
void "a discriminator type, an insert flag and an explicit value are carried over"() {
when:
Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenCoded, GenCodedChild)
@@ -2124,6 +2139,35 @@ class GenCar extends GenVehicle {
Integer doors
}
+@Entity
+class GenPrimVehicle {
+
+ String name
+ int wheels
+}
+
+@Entity
+class GenPrimCar extends GenPrimVehicle {
+
+ int doors
+ boolean sporty
+}
+
+@Entity
+class GenPrimJoinedRoot {
+
+ String name
+ static mapping = {
+ tablePerHierarchy false
+ }
+}
+
+@Entity
+class GenPrimJoinedChild extends GenPrimJoinedRoot {
+
+ int doors
+}
+
@Entity
class GenAnimal {