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:23 UTC
(grails-core) 06/29: feat: describe single-table inheritance 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 0e01aace4e8fbb0a8a45329634d1dd976c2ad85c
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Oct 4 23:55:27 2026 -0500
feat: describe single-table inheritance hierarchies in GrailsDomainGenerator
Hibernate's annotation binder derives a hierarchy from extends, so the generator
now builds the classes of a hierarchy together (generateAll): each subclass is
generated against its superclass and all of them are loaded into one class loader,
which a Hibernate registry can be given once. The root states @Inheritance,
@DiscriminatorColumn or @DiscriminatorFormula, @DiscriminatorOptions for a
non-inserted discriminator and @DiscriminatorValue; a subclass states only its
value and its own class-level facets, never a table.
Every decision is the binder's own accessor or constraint class; the differential
spec now compares root and subclass entities (kind, superclass, table, abstractness,
discriminator) with the bound PersistentClass and stays at zero mismatches.
Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
.../domainbinding/jpa/DiscriminatorFacets.groovy | 44 +++
.../domainbinding/jpa/GrailsDomainGenerator.groovy | 309 +++++++++++++++--
.../cfg/domainbinding/jpa/HierarchyFacets.groovy | 45 +++
.../GrailsDomainGeneratorDifferentialSpec.groovy | 91 ++++-
.../jpa/GrailsDomainGeneratorSpec.groovy | 385 ++++++++++++++++++++-
5 files changed, 829 insertions(+), 45 deletions(-)
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/DiscriminatorFacets.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/DiscriminatorFacets.groovy
new file mode 100644
index 0000000000..5ef69a3064
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/DiscriminatorFacets.groovy
@@ -0,0 +1,44 @@
+/*
+ * 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 groovy.transform.CompileStatic
+import jakarta.persistence.DiscriminatorType
+
+/**
+ * The discriminator the domain binder gives the root of a table-per-hierarchy tree: a column or a formula, the type
+ * it is read as, and whether the discriminator is written on insert.
+ *
+ * <p>Exactly one of {@code column} and {@code formula} is set. {@code length} and {@code sqlType} are {@code null}
+ * when the mapping does not state them. {@code typeName} is the Hibernate type name the binder puts on the
+ * discriminator value ({@code string} unless the mapping says otherwise) and {@code type} is the JPA discriminator
+ * type that stands for it.</p>
+ *
+ * @since 9.0
+ */
+@CompileStatic
+record DiscriminatorFacets(
+ String column,
+ String formula,
+ String typeName,
+ DiscriminatorType type,
+ Integer length,
+ String sqlType,
+ boolean insertable) {
+}
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 bc6a33e80f..94006762a3 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
@@ -23,22 +23,32 @@ import java.lang.reflect.Method
import groovy.transform.CompileStatic
import jakarta.persistence.Column as JpaColumn
+import jakarta.persistence.DiscriminatorColumn
+import jakarta.persistence.DiscriminatorType
+import jakarta.persistence.DiscriminatorValue
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
+import jakarta.persistence.Inheritance
+import jakarta.persistence.InheritanceType
+import jakarta.persistence.PrimaryKeyJoinColumn
import jakarta.persistence.Table
import jakarta.persistence.Version
import net.bytebuddy.ByteBuddy
import net.bytebuddy.description.annotation.AnnotationDescription
+import net.bytebuddy.description.modifier.TypeManifestation
import net.bytebuddy.description.modifier.Visibility
import net.bytebuddy.description.type.TypeDescription
import net.bytebuddy.dynamic.DynamicType
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy
+import org.hibernate.Length
import org.hibernate.annotations.BatchSize
import org.hibernate.annotations.ColumnDefault
import org.hibernate.annotations.ColumnTransformer
import org.hibernate.annotations.Comment
+import org.hibernate.annotations.DiscriminatorFormula
+import org.hibernate.annotations.DiscriminatorOptions
import org.hibernate.annotations.DynamicInsert
import org.hibernate.annotations.DynamicUpdate
import org.hibernate.annotations.Formula
@@ -55,6 +65,7 @@ import org.hibernate.type.spi.TypeConfiguration
import org.hibernate.usertype.UserType
import org.grails.orm.hibernate.cfg.ColumnConfig
+import org.grails.orm.hibernate.cfg.DiscriminatorConfig
import org.grails.orm.hibernate.cfg.HibernateSimpleIdentity
import org.grails.orm.hibernate.cfg.IdentityEnumType
import org.grails.orm.hibernate.cfg.Mapping
@@ -121,42 +132,127 @@ class GrailsDomainGenerator {
}
/**
- * @param entity the entity to describe
+ * Generates the class for an entity that is not part of an inheritance hierarchy.
+ *
+ * @param entity the entity to describe; it must be a root with no subclasses
* @param parent the class loader the generated class is loaded under; the class lives as long as it does
* @return a new class whose annotations describe the entity
+ * @throws IllegalArgumentException when the entity is part of a hierarchy, which {@link #generateAll} describes
*/
Class<?> generate(GrailsHibernatePersistentEntity entity, ClassLoader parent) {
- if (!entity.isRoot()) {
- throw new UnsupportedOperationException(
- "Entity [${entity.name}] is part of an inheritance hierarchy, which the generator does not support yet")
+ return generateAll([entity], parent).get(entity)
+ }
+
+ /**
+ * Generates one class for each entity, so that a hierarchy is described whole. Hibernate's annotation binder
+ * derives the hierarchy from {@code extends}, so the class generated for a subclass extends the class
+ * generated for its direct superclass.
+ *
+ * <p>All the classes of one call are loaded together into a single new class loader whose parent is
+ * {@code parent}: a subclass resolves its superclass by plain delegation inside that loader, and a Hibernate
+ * registry that must load the classes by name needs only that one loader
+ * ({@code BootstrapServiceRegistryBuilder.applyClassLoader(generated.classLoader)}). Nothing is injected into an
+ * existing loader, so no reflective access to {@code ClassLoader.defineClass} is needed, and the classes are
+ * collected together with the loader.</p>
+ *
+ * @param entities the entities to describe; every direct superclass and every direct subclass of an entity must
+ * be in the collection
+ * @param parent the class loader the generated classes are loaded under
+ * @return the generated class of each entity, in the order of {@code entities}
+ * @throws IllegalArgumentException when a hierarchy is not complete
+ * @throws UnsupportedOperationException when an entity uses something the generator cannot describe yet
+ */
+ Map<GrailsHibernatePersistentEntity, Class<?>> generateAll(
+ Collection<? extends GrailsHibernatePersistentEntity> entities, ClassLoader parent) {
+ Set<GrailsHibernatePersistentEntity> given = new LinkedHashSet<GrailsHibernatePersistentEntity>(entities)
+ for (GrailsHibernatePersistentEntity entity : given) {
+ requireWholeHierarchy(entity, given)
+ }
+ List<GrailsHibernatePersistentEntity> ordered = new ArrayList<GrailsHibernatePersistentEntity>(given)
+ ordered.sort { GrailsHibernatePersistentEntity a, GrailsHibernatePersistentEntity b -> depth(a) <=> depth(b) }
+
+ Map<GrailsHibernatePersistentEntity, DynamicType.Unloaded<?>> made = [:]
+ Map<TypeDescription, byte[]> types = [:]
+ for (GrailsHibernatePersistentEntity entity : ordered) {
+ TypeDescription superType = entity.isRoot() ?
+ TypeDescription.ForLoadedType.of(Object) : made.get(superEntity(entity, given)).typeDescription
+ DynamicType.Unloaded<?> unloaded = make(entity, superType)
+ made.put(entity, unloaded)
+ types.putAll(unloaded.allTypes)
+ }
+ Map<TypeDescription, Class<?>> loaded = ClassLoadingStrategy.Default.WRAPPER.load(parent, types)
+ Map<GrailsHibernatePersistentEntity, Class<?>> result = [:]
+ for (GrailsHibernatePersistentEntity entity : given) {
+ result.put(entity, loaded.get(made.get(entity).typeDescription))
+ }
+ return result
+ }
+
+ private static void requireWholeHierarchy(GrailsHibernatePersistentEntity entity, Set<GrailsHibernatePersistentEntity> given) {
+ if (!entity.isRoot() && superEntity(entity, given) == null) {
+ throw new IllegalArgumentException(
+ "Entity [${entity.name}] is part of an inheritance hierarchy: generate it together with its " +
+ 'superclass and subclasses (generateAll)')
+ }
+ for (GrailsHibernatePersistentEntity child : entity.childEntities) {
+ if (!given.any { GrailsHibernatePersistentEntity other -> other.javaClass == child.javaClass }) {
+ throw new IllegalArgumentException(
+ "Entity [${entity.name}] is part of an inheritance hierarchy: generate it together with its " +
+ "subclass [${child.name}] (generateAll)")
+ }
+ }
+ }
+
+ private static GrailsHibernatePersistentEntity superEntity(
+ GrailsHibernatePersistentEntity entity, Set<GrailsHibernatePersistentEntity> given) {
+ return given.find { GrailsHibernatePersistentEntity other -> other.javaClass == entity.javaClass.superclass }
+ }
+
+ private static int depth(GrailsHibernatePersistentEntity entity) {
+ int depth = 0
+ for (Class<?> type = entity.javaClass.superclass; type != null && type != Object; type = type.superclass) {
+ depth++
}
- DynamicType.Builder<Object> builder = new ByteBuddy()
- .subclass(Object)
+ return depth
+ }
+
+ private DynamicType.Unloaded<?> make(GrailsHibernatePersistentEntity entity, TypeDescription superType) {
+ HierarchyFacets hierarchy = hierarchyFacets(entity)
+ DynamicType.Builder<Object> builder = (DynamicType.Builder<Object>) new ByteBuddy()
+ .subclass(superType)
.name(generatedClassName(entity))
- .annotateType(classAnnotations(entity) as AnnotationDescription[])
+ .annotateType(classAnnotations(entity, hierarchy) as AnnotationDescription[])
+ if (hierarchy.abstractClass()) {
+ builder = builder.modifiers(Visibility.PUBLIC, TypeManifestation.ABSTRACT)
+ }
+ if (entity.isRoot()) {
+ builder = defineIdentityAndVersion(builder, entity)
+ }
+ for (HibernatePersistentProperty property : entity.persistentPropertiesToBind) {
+ if (!supports(property)) {
+ throw new UnsupportedOperationException(unsupportedReason(entity, property))
+ }
+ builder = defineField(builder, property, [])
+ }
+ return builder.make()
+ }
+ private DynamicType.Builder<Object> defineIdentityAndVersion(
+ DynamicType.Builder<Object> builder, GrailsHibernatePersistentEntity entity) {
HibernatePersistentProperty identity = (HibernatePersistentProperty) entity.identity
if (!(identity instanceof HibernateSimpleIdentityProperty)) {
throw new UnsupportedOperationException(
"Entity [${entity.name}] has no simple identifier (a composite identifier, for example), " +
'which the generator does not support yet')
}
- if (identity != null) {
- List<AnnotationDescription> idAnnotations = [AnnotationDescription.Builder.ofType(Id).build()]
- idAnnotations.addAll(idGeneratorAnnotations(idFacets(entity)))
- builder = defineField(builder, identity, idAnnotations)
- }
+ List<AnnotationDescription> idAnnotations = [AnnotationDescription.Builder.ofType(Id).build()]
+ idAnnotations.addAll(idGeneratorAnnotations(idFacets(entity)))
+ DynamicType.Builder<Object> result = defineField(builder, identity, idAnnotations)
HibernatePersistentProperty version = entity.version
if (version != null) {
- builder = defineField(builder, version, [AnnotationDescription.Builder.ofType(Version).build()])
- }
- for (HibernatePersistentProperty property : entity.persistentPropertiesToBind) {
- if (!supports(property)) {
- throw new UnsupportedOperationException(unsupportedReason(entity, property))
- }
- builder = defineField(builder, property, [])
+ result = defineField(result, version, [AnnotationDescription.Builder.ofType(Version).build()])
}
- return builder.make().load(parent, ClassLoadingStrategy.Default.WRAPPER).loaded
+ return result
}
private String unsupportedReason(GrailsHibernatePersistentEntity entity, HibernatePersistentProperty property) {
@@ -177,20 +273,123 @@ class GrailsDomainGenerator {
}
/**
- * Decides the class-level facets of a root entity the way the domain binder does.
+ * Decides the class-level facets of an entity the way the domain binder does. A single-table subclass has no
+ * table of its own: it reports the table of its hierarchy and no comment, which belongs to the table and so to
+ * the root. A subclass's JPA
+ * name is always its simple name, whatever {@code autoImport} says: that is what the binder's subclass mapping
+ * does.
*/
EntityFacets entityFacets(GrailsHibernatePersistentEntity entity) {
Mapping mapping = entity.mappedForm
+ boolean sharesTable = entity.isTablePerHierarchySubclass()
+ GrailsHibernatePersistentEntity tableOwner = sharesTable ? entity.hibernateRootEntity : entity
+ Mapping tableMapping = tableOwner.mappedForm
boolean autoImport = mapping == null || mapping.autoImport
return new EntityFacets(
- autoImport ? entity.javaClass.simpleName : entity.javaClass.name,
- entity.getTableName(namingStrategy),
- mapping?.table?.schema ?: null,
- mapping?.table?.catalog ?: null,
+ autoImport || !entity.isRoot() ? entity.javaClass.simpleName : entity.javaClass.name,
+ tableOwner.getTableName(namingStrategy),
+ tableMapping?.table?.schema ?: null,
+ tableMapping?.table?.catalog ?: null,
mapping != null && mapping.dynamicInsert,
mapping != null && mapping.dynamicUpdate,
mapping?.batchSize != null ? mapping.batchSize : 0,
- entity.comment)
+ sharesTable ? null : entity.comment)
+ }
+
+ /**
+ * Decides where the entity sits in its inheritance hierarchy the way the domain binder does: the strategy is the
+ * one the entity's own accessors report ({@code isUnionSubclass}, {@code isJoinedSubclass}, else single table),
+ * the discriminator is the one {@code DiscriminatorPropertyBinder} binds on the root of a single-table
+ * hierarchy that has subclasses, a subclass's discriminator value is the entity's own
+ * {@code getDiscriminatorValue}, and a joined subclass's key column is named like its identifier column.
+ *
+ * @throws UnsupportedOperationException for a hierarchy that mixes strategies (annotations state the strategy
+ * once, on the root) or whose discriminator has no annotation equivalent
+ */
+ HierarchyFacets hierarchyFacets(GrailsHibernatePersistentEntity entity) {
+ boolean root = entity.isRoot()
+ if (root && entity.childEntities.isEmpty()) {
+ return new HierarchyFacets(null, null, entity.isAbstract(), entity.isTableAbstract(), true, null, null, null)
+ }
+ InheritanceType strategy = inheritanceType(entity)
+ InheritanceType hierarchyStrategy = inheritanceType(entity.hibernateRootEntity)
+ if (strategy != hierarchyStrategy) {
+ throw new UnsupportedOperationException(
+ "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.SINGLE_TABLE) {
+ throw new UnsupportedOperationException("Inheritance strategy ${strategy} of [${entity.name}] is not supported yet")
+ }
+ boolean singleTable = strategy == InheritanceType.SINGLE_TABLE
+ String discriminatorValue = null
+ if (singleTable) {
+ DiscriminatorConfig config = entity.hibernateMappedForm?.discriminator
+ discriminatorValue = root ? (config?.value != null ? config.value : entity.name) : entity.discriminatorValue
+ }
+ return new HierarchyFacets(
+ strategy,
+ root ? null : entity.parentEntity.name,
+ entity.isAbstract(),
+ root ? entity.isTableAbstract() : strategy == InheritanceType.TABLE_PER_CLASS && entity.isAbstract(),
+ root || !singleTable,
+ discriminatorValue,
+ root && singleTable ? discriminatorFacets(entity) : null,
+ !root && strategy == InheritanceType.JOINED ?
+ columnNames.getColumnNameForPropertyAndPath((HibernatePersistentProperty) entity.identity, '', null) : null)
+ }
+
+ private static InheritanceType inheritanceType(GrailsHibernatePersistentEntity entity) {
+ if (entity.isUnionSubclass()) {
+ return InheritanceType.TABLE_PER_CLASS
+ }
+ return entity.isJoinedSubclass() ? InheritanceType.JOINED : InheritanceType.SINGLE_TABLE
+ }
+
+ /**
+ * Mirrors {@code ConfiguredDiscriminatorBinder} and {@code DefaultDiscriminatorBinder}: a formula, else a column
+ * that is named {@code class} unless configured and takes its length and SQL type from the column config.
+ */
+ private DiscriminatorFacets discriminatorFacets(GrailsHibernatePersistentEntity entity) {
+ DiscriminatorConfig config = entity.hibernateMappedForm?.discriminator
+ String typeName = config?.type == null ? 'string' :
+ (config.type instanceof Class ? ((Class<?>) config.type).name : config.type.toString())
+ DiscriminatorType type = discriminatorType(entity, typeName)
+ boolean insertable = config?.insertable == null || config.insertable
+ if (config?.formula != null) {
+ return new DiscriminatorFacets(null, config.formula, typeName, type, null, null, insertable)
+ }
+ ColumnConfig columnConfig = config?.column
+ Column column = new Column()
+ columnConfigBinder.bindColumnConfigToColumn(column, columnConfig, null)
+ if (column.precision != null || column.scale != null) {
+ throw new UnsupportedOperationException(
+ "The discriminator column of [${entity.name}] sets a precision or a scale, " +
+ 'which @DiscriminatorColumn cannot state')
+ }
+ return new DiscriminatorFacets(
+ columnConfig?.name != null ? columnConfig.name : GrailsDomainBinder.DEFAULT_DISCRIMINATOR_COLUMN_NAME,
+ null, typeName, type, column.length?.intValue(), column.sqlType, insertable)
+ }
+
+ private static DiscriminatorType discriminatorType(GrailsHibernatePersistentEntity entity, String typeName) {
+ switch (typeName) {
+ case 'string':
+ case 'java.lang.String':
+ return DiscriminatorType.STRING
+ case 'integer':
+ case 'int':
+ case 'java.lang.Integer':
+ return DiscriminatorType.INTEGER
+ case 'character':
+ case 'char':
+ case 'java.lang.Character':
+ return DiscriminatorType.CHAR
+ default:
+ throw new UnsupportedOperationException(
+ "The discriminator type [${typeName}] of [${entity.name}] is not one of string, integer " +
+ 'or character, which the generator does not support yet')
+ }
}
/**
@@ -433,19 +632,22 @@ class GrailsDomainGenerator {
return columns == null || columns.isEmpty() ? null : columns[0]
}
- private List<AnnotationDescription> classAnnotations(GrailsHibernatePersistentEntity entity) {
+ private List<AnnotationDescription> classAnnotations(GrailsHibernatePersistentEntity entity, HierarchyFacets hierarchy) {
EntityFacets facets = entityFacets(entity)
List<AnnotationDescription> annotations = []
annotations << AnnotationDescription.Builder.ofType(Entity).define('name', facets.jpaName()).build()
- AnnotationDescription.Builder table = AnnotationDescription.Builder.ofType(Table).define('name', facets.tableName())
- if (facets.schema()) {
- table = table.define('schema', facets.schema())
- }
- if (facets.catalog()) {
- table = table.define('catalog', facets.catalog())
+ if (hierarchy.ownsTable()) {
+ AnnotationDescription.Builder table = AnnotationDescription.Builder.ofType(Table).define('name', facets.tableName())
+ if (facets.schema()) {
+ table = table.define('schema', facets.schema())
+ }
+ if (facets.catalog()) {
+ table = table.define('catalog', facets.catalog())
+ }
+ annotations << table.build()
}
- annotations << table.build()
+ annotations.addAll(hierarchyAnnotations(entity, hierarchy))
if (facets.dynamicInsert()) {
annotations << AnnotationDescription.Builder.ofType(DynamicInsert).build()
@@ -462,6 +664,45 @@ class GrailsDomainGenerator {
return annotations
}
+ private static List<AnnotationDescription> hierarchyAnnotations(GrailsHibernatePersistentEntity entity, HierarchyFacets hierarchy) {
+ List<AnnotationDescription> annotations = []
+ if (hierarchy.strategy() == null) {
+ return annotations
+ }
+ if (entity.isRoot()) {
+ annotations << AnnotationDescription.Builder.ofType(Inheritance).define('strategy', hierarchy.strategy()).build()
+ }
+ DiscriminatorFacets discriminator = hierarchy.discriminator()
+ if (discriminator != null) {
+ if (discriminator.formula() != null) {
+ annotations << AnnotationDescription.Builder.ofType(DiscriminatorFormula)
+ .define('value', discriminator.formula())
+ .define('discriminatorType', discriminator.type())
+ .build()
+ } else {
+ AnnotationDescription.Builder column = AnnotationDescription.Builder.ofType(DiscriminatorColumn)
+ .define('name', discriminator.column())
+ .define('discriminatorType', discriminator.type())
+ // the binder's discriminator column has Hibernate's default length unless the mapping says otherwise
+ .define('length', discriminator.length() != null ? discriminator.length() : (int) Length.DEFAULT)
+ if (discriminator.sqlType()) {
+ column = column.define('columnDefinition', discriminator.sqlType())
+ }
+ annotations << column.build()
+ }
+ if (!discriminator.insertable()) {
+ annotations << AnnotationDescription.Builder.ofType(DiscriminatorOptions).define('insert', false).build()
+ }
+ }
+ if (hierarchy.discriminatorValue() != null) {
+ annotations << AnnotationDescription.Builder.ofType(DiscriminatorValue).define('value', hierarchy.discriminatorValue()).build()
+ }
+ if (hierarchy.keyColumn() != null) {
+ annotations << AnnotationDescription.Builder.ofType(PrimaryKeyJoinColumn).define('name', hierarchy.keyColumn()).build()
+ }
+ return annotations
+ }
+
private DynamicType.Builder<Object> defineField(
DynamicType.Builder<Object> builder, HibernatePersistentProperty property, List<AnnotationDescription> extra) {
List<AnnotationDescription> annotations = new ArrayList<>(extra)
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/HierarchyFacets.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/HierarchyFacets.groovy
new file mode 100644
index 0000000000..22d7e73eb5
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/HierarchyFacets.groovy
@@ -0,0 +1,45 @@
+/*
+ * 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 groovy.transform.CompileStatic
+import jakarta.persistence.InheritanceType
+
+/**
+ * The inheritance decisions the domain binder makes for one entity: where it sits in its hierarchy, whether it has
+ * a table of its own, and what tells its rows apart.
+ *
+ * <p>{@code strategy} is {@code null} for an entity that is not part of a hierarchy (a root with no subclasses).
+ * {@code superclass} is the entity name of the direct superclass, {@code null} for a root. {@code discriminator}
+ * is only set for the root of a single-table hierarchy that has subclasses, {@code discriminatorValue} only for the
+ * entities of a single-table hierarchy, and {@code keyColumn} only for a joined subclass.</p>
+ *
+ * @since 9.0
+ */
+@CompileStatic
+record HierarchyFacets(
+ InheritanceType strategy,
+ String superclass,
+ boolean abstractClass,
+ boolean abstractTable,
+ boolean ownsTable,
+ String discriminatorValue,
+ DiscriminatorFacets discriminator,
+ String keyColumn) {
+}
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 880cb4940b..478253ef63 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
@@ -31,6 +31,8 @@ import org.hibernate.mapping.Column
import org.hibernate.mapping.Formula
import org.hibernate.mapping.PersistentClass
import org.hibernate.mapping.Property
+import org.hibernate.mapping.RootClass
+import org.hibernate.mapping.SingleTableSubclass
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
@@ -87,6 +89,7 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
int derived = 0
Map<String, Integer> explicitTypes = [:].withDefault { 0 }
Map<String, Integer> strategies = [:].withDefault { 0 }
+ Map<String, Integer> hierarchies = [:].withDefault { 0 }
when:
for (List<Class<?>> group : groups) {
@@ -102,8 +105,18 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
.findAll { group.contains(it.javaClass) && it.persistentClass != null && it.persistentClass.entityName == it.name }) {
entities++
List<HibernatePersistentProperty> properties = []
+ HierarchyFacets hierarchy = null
+ try {
+ hierarchy = generator.hierarchyFacets(entity)
+ } catch (UnsupportedOperationException e) {
+ skipped["hierarchy: ${e.message.replaceAll(/\[[^\]]*\]/, '[..]')}".toString()]++
+ }
+ if (hierarchy != null) {
+ hierarchies["${hierarchy.strategy() ?: 'none'}${entity.isRoot() ? ' root' : ' subclass'}".toString()]++
+ mismatches.addAll(compareEntity(entity, generator.entityFacets(entity), hierarchy))
+ mismatches.addAll(compareHierarchy(entity, hierarchy))
+ }
if (entity.isRoot()) {
- mismatches.addAll(compareEntity(entity, generator.entityFacets(entity)))
if (entity.identity instanceof HibernateSimpleIdentityProperty) {
mismatches.addAll(compareIdentifierGenerator(
entity, generator.idFacets(entity), (SessionFactoryImplementor) datastore.sessionFactory, strategies))
@@ -113,9 +126,9 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
if (entity.version != null) {
properties << entity.version
}
- }
- if (entity.identity != null) {
- properties << (HibernatePersistentProperty) entity.identity
+ if (entity.identity != null) {
+ properties << (HibernatePersistentProperty) entity.identity
+ }
}
properties.addAll(entity.persistentPropertiesToBind)
for (HibernatePersistentProperty property : properties) {
@@ -161,6 +174,7 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
"(${derived} derived)\n"
report << "explicit types compared: ${explicitTypes}\n"
report << "id generators compared by strategy: ${strategies}\n"
+ report << "entities compared by hierarchy role: ${hierarchies}\n"
report << "unsupported by kind: ${skipped}\n"
report << "mismatches by facet: ${mismatches.groupBy { (it =~ /\s(\w+): generator=/)[0][1] }.collectEntries { k, v -> [k, v.size()] }}\n"
unbootable.each { report << "unbootable: ${it.key.take(120)} -> ${it.value.take(200)}\n" }
@@ -303,16 +317,19 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
return expected == actual ? [] : ["${entity.name}.${property.name} enumStyle: generator=${expected} binder=${actual}".toString()]
}
- private List<String> compareEntity(GrailsHibernatePersistentEntity entity, EntityFacets facets) {
+ private List<String> compareEntity(GrailsHibernatePersistentEntity entity, EntityFacets facets, HierarchyFacets hierarchy) {
PersistentClass persistentClass = entity.persistentClass
Map<String, List> pairs = [
jpaName : [facets.jpaName(), persistentClass.jpaEntityName],
tableName : [facets.tableName().replace('`', ''), persistentClass.table.name],
dynamicInsert: [facets.dynamicInsert(), persistentClass.useDynamicInsert()],
dynamicUpdate: [facets.dynamicUpdate(), persistentClass.useDynamicUpdate()],
- batchSize : [facets.batchSize(), persistentClass.batchSize],
- comment : [facets.comment(), persistentClass.table.comment],
+ // the binder leaves a subclass's unset batch size at -1 and a root's at 0: both mean "not stated"
+ batchSize : [facets.batchSize(), Math.max(persistentClass.batchSize, 0)],
]
+ if (hierarchy.ownsTable()) {
+ pairs.comment = [facets.comment(), persistentClass.table.comment]
+ }
if (facets.schema() != null) {
pairs.schema = [facets.schema(), persistentClass.table.schema]
}
@@ -324,6 +341,66 @@ class GrailsDomainGeneratorDifferentialSpec extends HibernateGormDatastoreSpec {
}
}
+ /**
+ * Where the entity sits in its hierarchy: the Hibernate class the binder created for it, its direct superclass,
+ * its table, abstractness and discriminator.
+ */
+ private List<String> compareHierarchy(GrailsHibernatePersistentEntity entity, HierarchyFacets facets) {
+ PersistentClass persistentClass = entity.persistentClass
+ Map<String, List> pairs = [
+ kind : [expectedKind(entity, facets), persistentClass.getClass().simpleName],
+ superclass : [facets.superclass(), persistentClass.superclass?.entityName],
+ abstractClass : [facets.abstractClass(), Boolean.TRUE == persistentClass.isAbstract()],
+ abstractTable : [facets.abstractTable(), persistentClass.table.isAbstract()],
+ ownsTable : [facets.ownsTable(),
+ persistentClass.superclass == null || !persistentClass.table.is(persistentClass.superclass.table)],
+ discriminatorValue: [facets.discriminatorValue(), persistentClass.discriminatorValue],
+ ]
+ List<String> found = pairs.findAll { String facet, List values -> values[0] != values[1] }.collect { String facet, List values ->
+ "${entity.name} ${facet}: generator=${values[0]} binder=${values[1]}".toString()
+ }
+ if (persistentClass instanceof RootClass) {
+ found.addAll(compareDiscriminator(entity, facets.discriminator(), (RootClass) persistentClass))
+ }
+ return found
+ }
+
+ private static String expectedKind(GrailsHibernatePersistentEntity entity, HierarchyFacets facets) {
+ if (entity.isRoot()) {
+ return RootClass.simpleName
+ }
+ return SingleTableSubclass.simpleName
+ }
+
+ /** The discriminator the binder put on the root: a column or a formula, its type, length and whether it is inserted. */
+ private List<String> compareDiscriminator(GrailsHibernatePersistentEntity entity, DiscriminatorFacets facets, RootClass root) {
+ String where = "${entity.name} discriminator"
+ if ((facets != null) != (root.discriminator != null)) {
+ return ["${where} present: generator=${facets != null} binder=${root.discriminator != null}".toString()]
+ }
+ if (facets == null) {
+ return []
+ }
+ BasicValue value = (BasicValue) root.discriminator
+ List<String> formulas = value.selectables.findAll { it instanceof Formula }.collect { ((Formula) it).getFormula() }
+ List<Column> columns = value.selectables.findAll { it instanceof Column }.collect { (Column) it }
+ Map<String, List> pairs = [
+ typeName : [facets.typeName(), value.typeName],
+ insertable: [facets.insertable(), root.isDiscriminatorInsertable()],
+ formula : [facets.formula() == null ? [] : [facets.formula()], formulas],
+ column : [facets.column() == null ? [] : [facets.column()], columns*.name],
+ ]
+ if (!columns.isEmpty()) {
+ pairs.length = [facets.length(), columns[0].length?.intValue()]
+ if (facets.sqlType() != null) {
+ pairs.sqlType = [facets.sqlType(), columns[0].sqlType]
+ }
+ }
+ return pairs.findAll { String facet, List values -> values[0] != values[1] }.collect { String facet, List values ->
+ "${where} ${facet}: generator=${values[0]} binder=${values[1]}".toString()
+ }
+ }
+
private static Property boundProperty(PersistentClass persistentClass, HibernatePersistentProperty property) {
if (persistentClass.identifierProperty != null && persistentClass.identifierProperty.name == property.name) {
return persistentClass.identifierProperty
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 51cf26854e..d7c097021b 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
@@ -27,15 +27,22 @@ import java.lang.reflect.Field
import grails.gorm.annotation.Entity
import grails.gorm.tests.HibernateGormDatastoreSpec
import jakarta.persistence.Column
+import jakarta.persistence.DiscriminatorColumn
+import jakarta.persistence.DiscriminatorType
+import jakarta.persistence.DiscriminatorValue
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
+import jakarta.persistence.Inheritance
+import jakarta.persistence.InheritanceType
import jakarta.persistence.Table
import jakarta.persistence.Version
import jakarta.validation.constraints.Size
import org.hibernate.annotations.BatchSize
import org.hibernate.annotations.ColumnDefault
import org.hibernate.annotations.Comment
+import org.hibernate.annotations.DiscriminatorFormula
+import org.hibernate.annotations.DiscriminatorOptions
import org.hibernate.annotations.DynamicUpdate
import org.hibernate.annotations.Formula
import org.hibernate.annotations.IdGeneratorType
@@ -53,6 +60,8 @@ import org.hibernate.id.enhanced.SequenceStyleGenerator
import org.hibernate.id.enhanced.TableGenerator
import org.hibernate.id.uuid.UuidGenerator as HibernateUuidGenerator
import org.hibernate.mapping.PersistentClass
+import org.hibernate.mapping.RootClass
+import org.hibernate.mapping.SingleTableSubclass
import org.hibernate.mapping.GeneratorCreator
import org.hibernate.boot.registry.BootstrapServiceRegistry
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
@@ -97,7 +106,15 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
void setupSpec() {
manager.registerDomainClasses(GenBasic, GenVehicle, GenCar, GenWithEnum, GenWithOwner, GenDerived, GenTyped, GenUnsupportedType, GenIdSequence, GenIdUuid, GenIdAssigned, GenIdTable,
- GenIdIncrement, GenIdIdentity, GenIdNative)
+ GenIdIncrement, GenIdIdentity, GenIdNative, GenAnimal, GenDog, GenPuppy, GenCat, GenToy, GenPlushToy, GenGadget,
+ GenGizmo, GenCoded, GenCodedChild, GenFormulaRoot, GenFormulaChild, GenAbstractBase, GenConcreteChild, GenNoted, GenNotedChild)
+ }
+
+ List<StandardServiceRegistry> registries = []
+
+ void cleanup() {
+ registries.each { StandardServiceRegistryBuilder.destroy(it) }
+ registries.clear()
}
void setup() {
@@ -162,13 +179,17 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
field('tag').getAnnotation(Size).max() == 20
}
- void "an entity in an inheritance hierarchy is rejected until inheritance is supported"() {
+ void "an entity of an inheritance hierarchy cannot be generated on its own"() {
when:
- generate(GenCar)
+ generate(entityClass)
then:
- UnsupportedOperationException e = thrown()
+ IllegalArgumentException e = thrown()
e.message.contains('inheritance hierarchy')
+ e.message.contains('generateAll')
+
+ where:
+ entityClass << [GenCar, GenVehicle]
}
void "a property the generator does not support is rejected by name"() {
@@ -404,6 +425,235 @@ class GrailsDomainGeneratorSpec extends HibernateGormDatastoreSpec {
StandardServiceRegistryBuilder.destroy(registry)
}
+ void "a single-table hierarchy is generated whole, each subclass extending the class generated for its superclass"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)
+
+ then:
+ classes[entity(GenAnimal)].superclass == Object
+ classes[entity(GenDog)].superclass == classes[entity(GenAnimal)]
+ classes[entity(GenPuppy)].superclass == classes[entity(GenDog)]
+ classes[entity(GenCat)].superclass == classes[entity(GenAnimal)]
+ classes.keySet()*.javaClass == [GenAnimal, GenDog, GenPuppy, GenCat]
+ }
+
+ void "the classes of one hierarchy share one class loader that can load each of them by name and see the parent"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)
+ ClassLoader loader = classes.values().first().classLoader
+
+ then:
+ classes.values().every { it.classLoader.is(loader) }
+ !loader.is(getClass().classLoader)
+ classes.values().every { Class.forName(it.name, false, loader).is(it) }
+ loader.parent.is(getClass().classLoader)
+
+ and: "the loader is not the loader of an unrelated generation"
+ !generate(GenBasic).classLoader.is(loader)
+ }
+
+ void "the root of a single-table hierarchy states the strategy and the default discriminator"() {
+ when:
+ Class<?> root = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)[entity(GenAnimal)]
+ DiscriminatorColumn column = root.getAnnotation(DiscriminatorColumn)
+
+ then:
+ root.getAnnotation(Inheritance).strategy() == InheritanceType.SINGLE_TABLE
+ root.getAnnotation(Table).name() == 'gen_animal'
+ column.name() == 'class'
+ column.discriminatorType() == DiscriminatorType.STRING
+ column.length() == 255
+ root.getAnnotation(DiscriminatorValue).value() == GenAnimal.name
+ !root.isAnnotationPresent(DiscriminatorOptions)
+ !root.isAnnotationPresent(DiscriminatorFormula)
+ }
+
+ void "a single-table subclass has no table, no strategy and no discriminator column, only its value"() {
+ given:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)
+
+ expect:
+ Class<?> subclass = classes[entity(entityClass)]
+ subclass.getAnnotation(DiscriminatorValue).value() == entityClass.name
+ subclass.getAnnotation(jakarta.persistence.Entity).name() == entityClass.simpleName
+ !subclass.isAnnotationPresent(Table)
+ !subclass.isAnnotationPresent(Inheritance)
+ !subclass.isAnnotationPresent(DiscriminatorColumn)
+
+ where:
+ entityClass << [GenDog, GenPuppy, GenCat]
+ }
+
+ void "a subclass declares only its own properties, with no identifier and no version, and its columns are nullable"() {
+ when:
+ Class<?> dog = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)[entity(GenDog)]
+
+ then:
+ dog.declaredFields*.name == ['breed']
+ !dog.getDeclaredField('breed').isAnnotationPresent(Id)
+ dog.getDeclaredField('breed').getAnnotation(Column).nullable()
+ dog.getDeclaredField('breed').getAnnotation(Column).name() == 'breed'
+ }
+
+ void "a configured discriminator states its value, column, length and the value of each subclass"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenToy, GenPlushToy)
+ Class<?> root = classes[entity(GenToy)]
+ DiscriminatorColumn column = root.getAnnotation(DiscriminatorColumn)
+
+ then:
+ root.getAnnotation(DiscriminatorValue).value() == 'TOY'
+ column.name() == 'toy_kind'
+ column.length() == 12
+ column.discriminatorType() == DiscriminatorType.STRING
+ classes[entity(GenPlushToy)].getAnnotation(DiscriminatorValue).value() == 'PLUSH'
+ }
+
+ void "a root mapping that sets only the discriminator column keeps the class name as the root value"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenGadget, GenGizmo)
+
+ then:
+ classes[entity(GenGadget)].getAnnotation(DiscriminatorColumn).name() == 'gadget_kind'
+ classes[entity(GenGadget)].getAnnotation(DiscriminatorValue).value() == GenGadget.name
+ classes[entity(GenGizmo)].getAnnotation(DiscriminatorValue).value() == GenGizmo.name
+ }
+
+ void "a discriminator type, an insert flag and an explicit value are carried over"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenCoded, GenCodedChild)
+ Class<?> root = classes[entity(GenCoded)]
+
+ then:
+ root.getAnnotation(DiscriminatorColumn).discriminatorType() == DiscriminatorType.INTEGER
+ root.getAnnotation(DiscriminatorColumn).name() == 'class'
+ root.getAnnotation(DiscriminatorOptions).insert() == false
+ root.getAnnotation(DiscriminatorValue).value() == '1'
+ classes[entity(GenCodedChild)].getAnnotation(DiscriminatorValue).value() == '2'
+ }
+
+ void "a discriminator formula replaces the discriminator column"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenFormulaRoot, GenFormulaChild)
+ Class<?> root = classes[entity(GenFormulaRoot)]
+
+ then:
+ root.getAnnotation(DiscriminatorFormula).value() == "case when kind_code = 1 then 'A' else 'B' end"
+ root.getAnnotation(DiscriminatorFormula).discriminatorType() == DiscriminatorType.STRING
+ !root.isAnnotationPresent(DiscriminatorColumn)
+ root.getAnnotation(DiscriminatorValue).value() == GenFormulaRoot.name
+ classes[entity(GenFormulaChild)].getAnnotation(DiscriminatorValue).value() == 'A'
+ }
+
+ void "an abstract class in a hierarchy is generated abstract"() {
+ when:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAbstractBase, GenConcreteChild, GenNoted, GenNotedChild)
+
+ then:
+ java.lang.reflect.Modifier.isAbstract(classes[entity(GenAbstractBase)].modifiers)
+ !java.lang.reflect.Modifier.isAbstract(classes[entity(GenConcreteChild)].modifiers)
+ classes[entity(GenConcreteChild)].superclass == classes[entity(GenAbstractBase)]
+ }
+
+ 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)
+ Class<?> root = classes[entity(GenNoted)]
+ Class<?> child = classes[entity(GenNotedChild)]
+
+ then:
+ root.getAnnotation(Comment).value() == 'noted things'
+ root.getAnnotation(Table).name() == 'gen_noted'
+ child.isAnnotationPresent(org.hibernate.annotations.DynamicInsert)
+ child.getAnnotation(BatchSize).size() == 3
+ !child.isAnnotationPresent(Comment)
+ !child.isAnnotationPresent(Table)
+ !root.isAnnotationPresent(org.hibernate.annotations.DynamicInsert)
+ }
+
+ void "a hierarchy must be generated whole"() {
+ when:
+ newGenerator().generateAll(entityClasses.collect { entity(it) }, getClass().classLoader)
+
+ then:
+ IllegalArgumentException e = thrown()
+ e.message.contains(missing)
+
+ where:
+ entityClasses | missing
+ [GenDog] | 'superclass'
+ [GenAnimal, GenDog] | 'subclass [org.grails.orm.hibernate.cfg.domainbinding.jpa.GenCat]'
+ }
+
+ void "the facets of a hierarchy are the ones the binder reports"() {
+ when:
+ HierarchyFacets facets = newGenerator().hierarchyFacets(entity(entityClass))
+
+ then:
+ facets.strategy() == strategy
+ facets.superclass() == superclass
+ facets.ownsTable() == ownsTable
+ facets.discriminatorValue() == value
+ (facets.discriminator() != null) == hasDiscriminator
+
+ where:
+ entityClass | strategy | superclass | ownsTable | value | hasDiscriminator
+ GenAnimal | InheritanceType.SINGLE_TABLE | null | true | GenAnimal.name | true
+ GenDog | InheritanceType.SINGLE_TABLE | GenAnimal.name | false | GenDog.name | false
+ GenPuppy | InheritanceType.SINGLE_TABLE | GenDog.name | false | GenPuppy.name | false
+ GenBasic | null | null | true | null | false
+ }
+
+ void "Hibernate's annotation binder reads the generated single-table hierarchy as the binder builds it"() {
+ given:
+ Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateHierarchy(GenAnimal, GenDog, GenPuppy, GenCat)
+
+ when:
+ Metadata metadata = annotationMetadata(classes.values())
+ PersistentClass animal = metadata.getEntityBinding(classes[entity(GenAnimal)].name)
+ PersistentClass dog = metadata.getEntityBinding(classes[entity(GenDog)].name)
+ PersistentClass puppy = metadata.getEntityBinding(classes[entity(GenPuppy)].name)
+
+ then:
+ animal instanceof RootClass
+ dog instanceof SingleTableSubclass
+ puppy instanceof SingleTableSubclass
+ puppy.superclass.entityName == dog.entityName
+ dog.superclass.entityName == animal.entityName
+ puppy.table.is(animal.table)
+ animal.table.name == 'gen_animal'
+ animal.discriminator.selectables*.text == ['class']
+ animal.discriminatorValue == GenAnimal.name
+ puppy.discriminatorValue == GenPuppy.name
+ puppy.getProperty('weeks').columns*.nullable == [true]
+
+ and: "the binder built the same shape"
+ getPersistentEntity(GenPuppy).persistentClass.superclass.entityName == GenDog.name
+ getPersistentEntity(GenAnimal).persistentClass.discriminator.selectables*.text == ['class']
+ }
+
+ private Metadata annotationMetadata(Collection<Class<?>> classes) {
+ BootstrapServiceRegistry bootstrap = new BootstrapServiceRegistryBuilder()
+ .applyClassLoader(classes.first().classLoader)
+ .build()
+ StandardServiceRegistry registry = new StandardServiceRegistryBuilder(bootstrap)
+ .applySetting('hibernate.dialect', H2Dialect.name)
+ .applySetting('hibernate.connection.url', 'jdbc:h2:mem:generator-hierarchy;DB_CLOSE_DELAY=-1')
+ .build()
+ registries << registry
+ MetadataSources sources = new MetadataSources(registry)
+ classes.each { sources.addAnnotatedClass(it) }
+ return sources.buildMetadata()
+ }
+
+ private Map<GrailsHibernatePersistentEntity, Class<?>> generateHierarchy(Class<?>... domainClasses) {
+ return newGenerator().generateAll(domainClasses.collect { entity(it) }, getClass().classLoader)
+ }
+
+ private GrailsHibernatePersistentEntity entity(Class<?> domainClass) {
+ return getPersistentEntity(domainClass)
+ }
+
private IdFacets idFacets(Class<?> domainClass) {
return newGenerator().idFacets(getPersistentEntity(domainClass))
}
@@ -503,6 +753,133 @@ class GenCar extends GenVehicle {
Integer doors
}
+@Entity
+class GenAnimal {
+
+ String name
+}
+
+@Entity
+class GenDog extends GenAnimal {
+
+ String breed
+
+ static constraints = {
+ breed nullable: false
+ }
+}
+
+@Entity
+class GenPuppy extends GenDog {
+
+ Integer weeks
+}
+
+@Entity
+class GenCat extends GenAnimal {
+
+ Boolean indoor
+}
+
+@Entity
+class GenToy {
+
+ String name
+
+ static mapping = {
+ discriminator value: 'TOY', column: [name: 'toy_kind', length: 12]
+ }
+}
+
+@Entity
+class GenPlushToy extends GenToy {
+
+ static mapping = {
+ discriminator 'PLUSH'
+ }
+}
+
+@Entity
+class GenGadget {
+
+ String name
+
+ static mapping = {
+ discriminator column: 'gadget_kind'
+ }
+}
+
+@Entity
+class GenGizmo extends GenGadget {
+}
+
+@Entity
+class GenCoded {
+
+ String name
+
+ static mapping = {
+ discriminator value: '1', type: 'integer', insert: false
+ }
+}
+
+@Entity
+class GenCodedChild extends GenCoded {
+
+ static mapping = {
+ discriminator '2'
+ }
+}
+
+@Entity
+class GenFormulaRoot {
+
+ Integer kindCode
+
+ static mapping = {
+ discriminator formula: "case when kind_code = 1 then 'A' else 'B' end"
+ }
+}
+
+@Entity
+class GenFormulaChild extends GenFormulaRoot {
+
+ static mapping = {
+ discriminator 'A'
+ }
+}
+
+@Entity
+class GenNoted {
+
+ String name
+
+ static mapping = {
+ comment 'noted things'
+ }
+}
+
+@Entity
+class GenNotedChild extends GenNoted {
+
+ static mapping = {
+ dynamicInsert true
+ batchSize 3
+ }
+}
+
+@Entity
+abstract class GenAbstractBase {
+
+ String title
+}
+
+@Entity
+class GenConcreteChild extends GenAbstractBase {
+
+ String extra
+}
+
enum GenKind {
SMALL, LARGE
}