diff --git a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java
index 99f034b2ef0a..d9f98aa30d52 100644
--- a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java
+++ b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java
@@ -8,6 +8,7 @@
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -17,6 +18,7 @@
import org.hibernate.AssertionFailure;
import org.hibernate.annotations.NotFoundAction;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.boot.MappingException;
import org.hibernate.boot.internal.LimitedCollectionClassification;
import org.hibernate.boot.jaxb.Origin;
@@ -38,7 +40,6 @@
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmCustomSqlDmlType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmDiscriminatorSubclassEntityType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmDynamicComponentType;
-import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmDiscriminatorSubclassEntityType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmEntityBaseDefinition;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmFetchProfileType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmFetchStyleEnum;
@@ -58,6 +59,7 @@
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmManyToAnyCollectionElementType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmManyToManyCollectionElementType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmManyToOneType;
+import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmOnDeleteEnum;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmMapKeyBasicType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmMapType;
import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmNamedNativeQueryType;
@@ -126,6 +128,7 @@
import org.hibernate.boot.jaxb.mapping.spi.JaxbGenericIdGeneratorImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbHqlImportImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbIdImpl;
+import org.hibernate.boot.jaxb.mapping.spi.JaxbIdClassImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbIndexImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbInheritanceImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbJoinTableImpl;
@@ -133,6 +136,8 @@
import org.hibernate.boot.jaxb.mapping.spi.JaxbManyToOneImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbMapKeyColumnImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbMapKeyJoinColumnImpl;
+import org.hibernate.boot.jaxb.mapping.spi.JaxbMappedSuperclassImpl;
+import org.hibernate.boot.jaxb.mapping.spi.JaxbPersistentAttribute;
import org.hibernate.boot.jaxb.mapping.spi.JaxbNamedNativeQueryImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbNamedHqlQueryImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbNaturalIdImpl;
@@ -366,11 +371,316 @@ private void performTransformation() {
defineInheritance( rootMappingEntity, InheritanceType.TABLE_PER_CLASS );
} );
+ generateMappedSuperclassesForUnmappedSuperclasses( mappingXmlRoot );
+
if ( TRANSFORMATION_LOGGER.isDebugEnabled() ) {
dumpTransformed( origin(), mappingXmlRoot );
}
}
+ /**
+ * Bridges the gap between hbm.xml and orm.xml attribute-member resolution.
+ *
+ * In hbm.xml, an entity can map properties whose Java member is declared on a plain
+ * (unmapped) superclass — the hbm processor walks the class hierarchy via reflection.
+ * In orm.xml, the XML processor requires each attribute's member to be declared on
+ * the entity class itself or on an explicitly declared {@code }.
+ *
+ * This method detects inherited attributes, generates {@code }
+ * elements for unmapped Java superclasses, moves the inherited attributes there,
+ * and marks remaining unmapped properties as {@code }.
+ */
+ private void generateMappedSuperclassesForUnmappedSuperclasses(JaxbEntityMappingsImpl mappingXmlRoot) {
+ final Set mappedEntityClassNames = collectMappedEntityClassNames();
+ final boolean fieldAccess = isFieldAccessDefault();
+
+ final Map generatedSuperclasses = new HashMap<>();
+ final Map> superclassJavaTypes = new HashMap<>();
+
+ for ( var entry : transformationState.getEntityInfoByName().entrySet() ) {
+ final Class> javaClass = resolveMappedClass( entry.getValue().getPersistentClass() );
+ final var entity = transformationState.getMappingEntityByName().get( entry.getKey() );
+ final var attrs = entity != null ? entity.getAttributes() : null;
+
+ if ( javaClass != null && attrs != null ) {
+ Class> currentSuperclass = javaClass.getSuperclass();
+ while ( currentSuperclass != null
+ && currentSuperclass != Object.class
+ && !mappedEntityClassNames.contains( currentSuperclass.getName() ) ) {
+ final var mappedSuperclass = getOrCreateMappedSuperclass(
+ currentSuperclass, fieldAccess,
+ generatedSuperclasses, superclassJavaTypes, mappingXmlRoot );
+ moveInheritedAttributesToSuperclass(
+ entity, attrs, mappedSuperclass, currentSuperclass, fieldAccess );
+ currentSuperclass = currentSuperclass.getSuperclass();
+ }
+ }
+ }
+
+ addTransientsForUnmappedProperties( generatedSuperclasses, superclassJavaTypes, fieldAccess );
+ }
+
+ /**
+ * Collects the class names of all already-mapped entities so that we never
+ * generate a {@code } for a class that is itself an entity.
+ */
+ private Set collectMappedEntityClassNames() {
+ final Set classNames = new HashSet<>();
+ for ( var entityInfo : transformationState.getEntityInfoByName().values() ) {
+ final String className = entityInfo.getPersistentClass().getClassName();
+ if ( className != null ) {
+ classNames.add( className );
+ }
+ }
+ return classNames;
+ }
+
+ /**
+ * Returns {@code true} when the hbm.xml mapping's default access type is {@code "field"}.
+ */
+ private boolean isFieldAccessDefault() {
+ final String access = hbmXmlBinding.getRoot().getDefaultAccess();
+ return "field".equals( access != null ? access.toLowerCase( Locale.ROOT ) : "property" );
+ }
+
+ /**
+ * Resolves the Java class mapped by the given {@link PersistentClass},
+ * returning {@code null} when the class is not on the classpath
+ * (e.g. in the reverse-engineering tooling scenario).
+ */
+ private static Class> resolveMappedClass(PersistentClass persistentClass) {
+ try {
+ return persistentClass.getMappedClass();
+ }
+ catch (org.hibernate.MappingException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Looks up or creates a {@link JaxbMappedSuperclassImpl} for the given unmapped
+ * superclass, registering it in the tracking maps and adding it to the mapping root.
+ * When multiple entities share the same superclass, the existing instance is returned.
+ */
+ private static JaxbMappedSuperclassImpl getOrCreateMappedSuperclass(
+ Class> superclass,
+ boolean fieldAccess,
+ Map generatedSuperclasses,
+ Map> superclassJavaTypes,
+ JaxbEntityMappingsImpl mappingXmlRoot) {
+ final String superclassName = superclass.getName();
+ var mappedSuperclass = generatedSuperclasses.get( superclassName );
+ if ( mappedSuperclass == null ) {
+ mappedSuperclass = new JaxbMappedSuperclassImpl();
+ mappedSuperclass.setClazz( superclassName );
+ mappedSuperclass.setMetadataComplete( true );
+ mappedSuperclass.setAccess( fieldAccess
+ ? jakarta.persistence.AccessType.FIELD
+ : jakarta.persistence.AccessType.PROPERTY );
+ mappedSuperclass.setAttributes( new JaxbAttributesContainerImpl() );
+ generatedSuperclasses.put( superclassName, mappedSuperclass );
+ superclassJavaTypes.put( superclassName, superclass );
+ mappingXmlRoot.getMappedSuperclasses().add( mappedSuperclass );
+ }
+ return mappedSuperclass;
+ }
+
+ /**
+ * Moves attributes (id, basic, associations, embedded, version, embedded-id)
+ * from the entity to the mapped-superclass when the member is declared on the
+ * given Java superclass. Also relocates the id-class when all id attributes
+ * have been moved, and removes entity-level transients for members that now
+ * belong to the superclass.
+ */
+ private static void moveInheritedAttributesToSuperclass(
+ JaxbEntityImpl entity,
+ JaxbAttributesContainerImpl attrs,
+ JaxbMappedSuperclassImpl mappedSuperclass,
+ Class> superclass,
+ boolean fieldAccess) {
+ final var superAttrs = mappedSuperclass.getAttributes();
+
+ moveInheritedAttributes( attrs.getIdAttributes(), superAttrs.getIdAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getBasicAttributes(), superAttrs.getBasicAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getManyToOneAttributes(), superAttrs.getManyToOneAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getOneToManyAttributes(), superAttrs.getOneToManyAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getOneToOneAttributes(), superAttrs.getOneToOneAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getManyToManyAttributes(), superAttrs.getManyToManyAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getEmbeddedAttributes(), superAttrs.getEmbeddedAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getElementCollectionAttributes(), superAttrs.getElementCollectionAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getAnyMappingAttributes(), superAttrs.getAnyMappingAttributes(), superclass, fieldAccess );
+ moveInheritedAttributes( attrs.getPluralAnyMappingAttributes(), superAttrs.getPluralAnyMappingAttributes(), superclass, fieldAccess );
+
+ moveVersionIfInherited( attrs, superAttrs, superclass, fieldAccess );
+ moveEmbeddedIdIfInherited( attrs, superAttrs, superclass, fieldAccess );
+ moveIdClassIfAllIdsMoved( entity, attrs, mappedSuperclass, superAttrs );
+
+ attrs.getTransients().removeIf( t -> isMemberDeclaredOnClass( t.getName(), superclass, fieldAccess ) );
+ }
+
+ /**
+ * Moves the version attribute from the entity to the mapped-superclass
+ * when the version member is declared on the given superclass.
+ */
+ private static void moveVersionIfInherited(
+ JaxbAttributesContainerImpl attrs,
+ JaxbAttributesContainerImpl superAttrs,
+ Class> superclass,
+ boolean fieldAccess) {
+ final var version = attrs.getVersion();
+ if ( version != null
+ && isMemberDeclaredOnClass( version.getName(), superclass, fieldAccess ) ) {
+ if ( superAttrs.getVersion() == null ) {
+ superAttrs.setVersion( version );
+ }
+ attrs.setVersion( null );
+ }
+ }
+
+ /**
+ * Moves the embedded-id attribute from the entity to the mapped-superclass
+ * when the embedded-id member is declared on the given superclass.
+ */
+ private static void moveEmbeddedIdIfInherited(
+ JaxbAttributesContainerImpl attrs,
+ JaxbAttributesContainerImpl superAttrs,
+ Class> superclass,
+ boolean fieldAccess) {
+ final var embeddedId = attrs.getEmbeddedIdAttribute();
+ if ( embeddedId != null
+ && isMemberDeclaredOnClass( embeddedId.getName(), superclass, fieldAccess ) ) {
+ if ( superAttrs.getEmbeddedIdAttribute() == null ) {
+ superAttrs.setEmbeddedIdAttribute( embeddedId );
+ }
+ attrs.setEmbeddedIdAttribute( null );
+ }
+ }
+
+ /**
+ * Relocates the id-class declaration from the entity to the mapped-superclass
+ * when all id attributes have already been moved there.
+ */
+ private static void moveIdClassIfAllIdsMoved(
+ JaxbEntityImpl entity,
+ JaxbAttributesContainerImpl attrs,
+ JaxbMappedSuperclassImpl mappedSuperclass,
+ JaxbAttributesContainerImpl superAttrs) {
+ if ( entity.getIdClass() != null
+ && mappedSuperclass.getIdClass() == null
+ && attrs.getIdAttributes().isEmpty()
+ && attrs.getEmbeddedIdAttribute() == null
+ && !superAttrs.getIdAttributes().isEmpty() ) {
+ mappedSuperclass.setIdClass( entity.getIdClass() );
+ entity.setIdClass( null );
+ }
+ }
+
+ /**
+ * Adds {@code } declarations for properties on each generated
+ * mapped-superclass that are not mapped by any entity.
+ */
+ private static void addTransientsForUnmappedProperties(
+ Map generatedSuperclasses,
+ Map> superclassJavaTypes,
+ boolean fieldAccess) {
+ for ( var entry : generatedSuperclasses.entrySet() ) {
+ final var mappedSuperclass = entry.getValue();
+ final var superAttrs = mappedSuperclass.getAttributes();
+ final Set mappedNames = collectMappedAttributeNames( superAttrs );
+ final Class> superclass = superclassJavaTypes.get( entry.getKey() );
+ if ( superclass != null ) {
+ final Set transientNames = TransformationHelper.discoverUnmappedPropertyNames(
+ superclass, mappedNames, fieldAccess
+ );
+ TransformationHelper.addTransients( transientNames, superAttrs.getTransients() );
+ }
+ }
+ }
+
+ /**
+ * Moves attributes whose member is declared on the given Java superclass
+ * from the entity's attribute list to the mapped-superclass attribute list.
+ * Duplicates (already present in the superclass) are skipped.
+ */
+ private static void moveInheritedAttributes(
+ List entityAttrs,
+ List superAttrs,
+ Class> javaSuperclass,
+ boolean fieldAccess) {
+ final var toMove = new ArrayList();
+ for ( var attr : entityAttrs ) {
+ if ( isMemberDeclaredOnClass( attr.getName(), javaSuperclass, fieldAccess ) ) {
+ toMove.add( attr );
+ }
+ }
+ final Set existingNames = new HashSet<>();
+ for ( var attr : superAttrs ) {
+ existingNames.add( attr.getName() );
+ }
+ for ( var attr : toMove ) {
+ entityAttrs.remove( attr );
+ if ( existingNames.add( attr.getName() ) ) {
+ superAttrs.add( attr );
+ }
+ }
+ }
+
+ private static Set collectMappedAttributeNames(JaxbAttributesContainerImpl attrs) {
+ final Set names = new HashSet<>();
+ addAttributeNames( names,
+ attrs.getIdAttributes(),
+ attrs.getBasicAttributes(),
+ attrs.getManyToOneAttributes(),
+ attrs.getOneToManyAttributes(),
+ attrs.getOneToOneAttributes(),
+ attrs.getManyToManyAttributes(),
+ attrs.getEmbeddedAttributes(),
+ attrs.getElementCollectionAttributes(),
+ attrs.getAnyMappingAttributes(),
+ attrs.getPluralAnyMappingAttributes()
+ );
+ if ( attrs.getVersion() != null ) {
+ names.add( attrs.getVersion().getName() );
+ }
+ if ( attrs.getEmbeddedIdAttribute() != null ) {
+ names.add( attrs.getEmbeddedIdAttribute().getName() );
+ }
+ return names;
+ }
+
+ @SafeVarargs
+ private static void addAttributeNames(Set names, List extends JaxbPersistentAttribute>... attrLists) {
+ for ( var attrList : attrLists ) {
+ for ( var attr : attrList ) {
+ names.add( attr.getName() );
+ }
+ }
+ }
+
+ /**
+ * Check if a field or getter for the given property name is declared directly
+ * on the given class (not inherited).
+ */
+ private static boolean isMemberDeclaredOnClass(String propertyName, Class> clazz, boolean fieldAccess) {
+ if ( fieldAccess ) {
+ try {
+ clazz.getDeclaredField( propertyName );
+ return true;
+ }
+ catch (NoSuchFieldException ignored) {
+ return false;
+ }
+ }
+ else {
+ for ( var method : clazz.getDeclaredMethods() ) {
+ if ( propertyName.equals( TransformationHelper.extractPropertyName( method ) ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
private static void dumpTransformed(Origin origin, JaxbEntityMappingsImpl ormRoot) {
try {
var ctx = JAXBContext.newInstance( JaxbEntityMappingsImpl.class );
@@ -776,7 +1086,9 @@ private void transferImports() {
final var ormImport = new JaxbHqlImportImpl();
ormRoot.getHqlImports().add( ormImport );
ormImport.setClazz( hbmImport.getClazz() );
- ormImport.setRename( hbmImport.getRename() );
+ final String rename = hbmImport.getRename();
+ // In hbm.xml rename is optional and defaults to the unqualified class name
+ ormImport.setRename( rename != null ? rename : StringHelper.unqualify( hbmImport.getClazz() ) );
}
}
}
@@ -1903,6 +2215,10 @@ public void addFormula(String formula) {
jaxbManyToOne.setForeignKey( transformForeignKey( hbmNode.getForeignKey() ) );
+ if ( hbmNode.getOnDelete() == JaxbHbmOnDeleteEnum.CASCADE ) {
+ jaxbManyToOne.setOnDelete( OnDeleteAction.CASCADE );
+ }
+
if ( hbmNode.getNotFound() != null ) {
jaxbManyToOne.setNotFound( interpretNotFoundAction( hbmNode.getNotFound() ) );
}
@@ -2050,15 +2366,37 @@ private void transferCollectionCommonInfo(PluralAttributeInfo source, JaxbPlural
target::setAccess,
target::setAttributeAccessor
);
- target.setFetchMode( convert( source.getFetch(), source.getOuterJoin() ) );
- target.setFetch( convert( source.getLazy() ) );
+ final var fetchMode = convert( source.getFetch(), source.getOuterJoin() );
+ final var fetchType = convert( source.getLazy() );
+ // In hbm.xml, fetch="join" and lazy="true" are independent: the collection stays lazy
+ // and uses join fetching only when initialized. In orm.xml, fetch-mode="JOIN" maps to
+ // @Fetch(FetchMode.JOIN) which forces eager loading (CollectionBinder overrides lazy to
+ // false for JOIN). Since a lazy collection uses a separate SELECT regardless of fetch
+ // style, we drop fetch-mode="JOIN" for lazy collections to preserve the lazy semantics.
+ if ( fetchMode != JaxbPluralFetchModeImpl.JOIN || fetchType != FetchType.LAZY ) {
+ target.setFetchMode( fetchMode );
+ }
+ target.setFetch( fetchType );
target.setOptimisticLock( source.isOptimisticLock() );
target.setMutable( source.isMutable() );
if ( isNotEmpty( source.getCollectionType() ) ) {
final var jaxbCollectionUserType = new JaxbCollectionUserTypeImpl();
target.setCollectionType( jaxbCollectionUserType );
- jaxbCollectionUserType.setType( source.getCollectionType() );
+ // resolve typedef alias to the actual implementation class and transfer parameters
+ final var typeDef = transformationState.getTypeDefMap().get( source.getCollectionType() );
+ if ( typeDef != null ) {
+ jaxbCollectionUserType.setType( typeDef.getClazz() );
+ for ( var param : typeDef.getConfigParameters() ) {
+ final var jaxbParam = new JaxbConfigurationParameterImpl();
+ jaxbParam.setName( param.getName() );
+ jaxbParam.setValue( param.getValue() );
+ jaxbCollectionUserType.getParameters().add( jaxbParam );
+ }
+ }
+ else {
+ jaxbCollectionUserType.setType( source.getCollectionType() );
+ }
}
if ( source instanceof JaxbHbmSetType set ) {
@@ -3255,7 +3593,7 @@ private JaxbManyToOneImpl transformCompositeKeyManyToOne(
}
jaxbKyManyToOne.setOptional( false );
- jaxbKyManyToOne.setFetch( FetchType.EAGER );
+ jaxbKyManyToOne.setFetch( FetchType.LAZY );
jaxbKyManyToOne.setFetchMode( JaxbSingularFetchModeImpl.SELECT );
jaxbKyManyToOne.setNotFound( NotFoundAction.EXCEPTION );
@@ -3369,7 +3707,9 @@ private void transferIdClass(
JaxbHbmCompositeIdType hbmCompositeId,
Component idClassMapping,
JaxbEntityImpl mappingXmlEntity) {
- throw new UnsupportedOperationException( "Not implemented yet" );
+ final var idClass = new JaxbIdClassImpl();
+ idClass.setClazz( getFullyQualifiedClassName( hbmCompositeId.getClazz() ) );
+ mappingXmlEntity.setIdClass( idClass );
}
private JaxbIdImpl transformNonAggregatedKeyProperty(
diff --git a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/TransformationHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/TransformationHelper.java
index 6f328d158b62..bacfd15496c2 100644
--- a/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/TransformationHelper.java
+++ b/hibernate-core/src/main/java/org/hibernate/boot/jaxb/hbm/transform/TransformationHelper.java
@@ -4,6 +4,7 @@
*/
package org.hibernate.boot.jaxb.hbm.transform;
+import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -47,6 +48,21 @@ static Set discoverAllPropertyNames(Class> javaClass, boolean fieldAcc
return discoverUnmappedPropertyNames( javaClass, Set.of(), fieldAccess );
}
+ static String extractPropertyName(Method method) {
+ if ( method.getParameterCount() != 0 ) {
+ return null;
+ }
+ final String methodName = method.getName();
+ if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) {
+ return StringHelper.decapitalize( methodName.substring( 3 ) );
+ }
+ if ( methodName.startsWith( "is" ) && methodName.length() > 2
+ && ( method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class ) ) {
+ return StringHelper.decapitalize( methodName.substring( 2 ) );
+ }
+ return null;
+ }
+
static Set discoverUnmappedPropertyNames(
Class> javaClass,
Set mappedPropertyNames,
@@ -68,18 +84,7 @@ static Set discoverUnmappedPropertyNames(
effectiveMappedNames.add( StringHelper.decapitalize( name ) );
}
for ( var method : javaClass.getMethods() ) {
- if ( method.getParameterCount() != 0 ) {
- continue;
- }
- String propertyName = null;
- final String methodName = method.getName();
- if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) {
- propertyName = StringHelper.decapitalize( methodName.substring( 3 ) );
- }
- else if ( methodName.startsWith( "is" ) && methodName.length() > 2
- && ( method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class ) ) {
- propertyName = StringHelper.decapitalize( methodName.substring( 2 ) );
- }
+ final String propertyName = extractPropertyName( method );
if ( propertyName != null
&& !effectiveMappedNames.contains( propertyName )
&& !propertyName.equals( "class" ) ) {
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/HbmTransformationJaxbTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/HbmTransformationJaxbTests.java
index c058f8a4060d..85c57158e0c4 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/HbmTransformationJaxbTests.java
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/HbmTransformationJaxbTests.java
@@ -26,9 +26,11 @@
import org.hibernate.boot.jaxb.mapping.spi.JaxbEmbeddableImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEmbeddedImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityImpl;
+import org.hibernate.boot.jaxb.mapping.spi.JaxbHqlImportImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityMappingsImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbIdImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbManyToManyImpl;
+import org.hibernate.boot.jaxb.mapping.spi.JaxbMappedSuperclassImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbManyToOneImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbOneToManyImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbOneToOneImpl;
@@ -777,6 +779,91 @@ public void testSharedEmbeddableFormulaPropertyTransformation(ServiceRegistrySco
} );
}
+ @Test
+ @JiraKey( "HHH-20709" )
+ public void testCollectionFetchJoinLazyTransformation(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml", scope, (transformed) -> {
+ final JaxbEntityImpl userEntity = transformed.getEntities().stream()
+ .filter( e -> "User".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ assertThat( userEntity.getAttributes().getOneToManyAttributes() ).hasSize( 1 );
+
+ final JaxbOneToManyImpl emailAddresses = userEntity.getAttributes().getOneToManyAttributes().get( 0 );
+ assertThat( emailAddresses.getName() ).isEqualTo( "emailAddresses" );
+ assertThat( emailAddresses.getFetchMode() )
+ .as( "Lazy collection with fetch='join' should not have fetch-mode=JOIN" )
+ .isNotEqualTo( org.hibernate.boot.jaxb.mapping.spi.JaxbPluralFetchModeImpl.JOIN );
+ assertThat( emailAddresses.getFetch() )
+ .as( "Collection with default lazy='true' should have fetch=LAZY" )
+ .isEqualTo( jakarta.persistence.FetchType.LAZY );
+ } );
+ }
+
+ @Test
+ @JiraKey( "HHH-20712" )
+ public void testOnDeleteCascadeTransformation(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/on-delete-toone/hbm.xml", scope, (transformed) -> {
+ final JaxbEntityImpl childEntity = transformed.getEntities().stream()
+ .filter( e -> "Child".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ assertThat( childEntity.getAttributes().getManyToOneAttributes() ).hasSize( 1 );
+
+ final JaxbManyToOneImpl parentManyToOne = childEntity.getAttributes().getManyToOneAttributes().get( 0 );
+ assertThat( parentManyToOne.getName() ).isEqualTo( "parent" );
+ assertThat( parentManyToOne.getOnDelete() )
+ .as( "many-to-one with on-delete='cascade' should have on-delete=CASCADE" )
+ .isEqualTo( org.hibernate.annotations.OnDeleteAction.CASCADE );
+ } );
+ }
+
+ @Test
+ @JiraKey( "HHH-20709" )
+ public void testCollectionFetchJoinEagerTransformation(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml", scope, (transformed) -> {
+ final JaxbEntityImpl userEntity = transformed.getEntities().stream()
+ .filter( e -> "User".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ assertThat( userEntity.getAttributes().getOneToManyAttributes() ).hasSize( 1 );
+
+ final JaxbOneToManyImpl emailAddresses = userEntity.getAttributes().getOneToManyAttributes().get( 0 );
+ assertThat( emailAddresses.getName() ).isEqualTo( "emailAddresses" );
+ assertThat( emailAddresses.getFetchMode() )
+ .as( "Eager collection with fetch='join' should have fetch-mode=JOIN" )
+ .isEqualTo( org.hibernate.boot.jaxb.mapping.spi.JaxbPluralFetchModeImpl.JOIN );
+ assertThat( emailAddresses.getFetch() )
+ .as( "Collection with lazy='false' should have fetch=EAGER" )
+ .isEqualTo( jakarta.persistence.FetchType.EAGER );
+ } );
+ }
+
+ @Test
+ @JiraKey( "HHH-20711" )
+ public void testIdClassTransformation(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/id-class/hbm.xml", scope, (transformed) -> {
+ final JaxbEntityImpl customerEntity = transformed.getEntities().stream()
+ .filter( e -> "Customer".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ assertThat( customerEntity.getIdClass() )
+ .as( "Entity with composite-id class should have id-class" )
+ .isNotNull();
+ assertThat( customerEntity.getIdClass().getClazz() )
+ .as( "id-class should reference the fully qualified CustomerId class" )
+ .isEqualTo( "org.hibernate.orm.test.idclass.CustomerId" );
+
+ assertThat( customerEntity.getAttributes().getIdAttributes() )
+ .as( "Entity should have 2 id attributes" )
+ .hasSize( 2 );
+ } );
+ }
+
private void transformAndVerify(
String resourceName,
ServiceRegistryScope scope,
@@ -1216,6 +1303,92 @@ public void testInverseOneToManyWithCompositeKeyPropertyMappedBy(ServiceRegistry
} );
}
+ @Test
+ @JiraKey( "HHH-20715" )
+ public void testUnmappedSuperclassGeneratesMappedSuperclass(ServiceRegistryScope scope) {
+ // ConcreteEntity and AnotherEntity both extend AbstractBase (a plain Java class, not an entity).
+ // The hbm.xml maps properties (id, version, name, relatedBase) declared on AbstractBase.
+ // The transformer should generate a single for AbstractBase,
+ // move the inherited attributes there, and mark unmapped superclass properties as transient.
+ transformAndVerify( "xml/jaxb/mapping/unmapped-superclass/hbm.xml", scope, (transformed) -> {
+ // A single mapped-superclass should be generated even though two entities share the same superclass
+ assertThat( transformed.getMappedSuperclasses() )
+ .as( "Exactly one should be generated for the shared AbstractBase" )
+ .hasSize( 1 );
+
+ final JaxbMappedSuperclassImpl mappedSuperclass = transformed.getMappedSuperclasses().get( 0 );
+ assertThat( mappedSuperclass.getClazz() )
+ .isEqualTo( "org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass.AbstractBase" );
+ assertThat( mappedSuperclass.isMetadataComplete() ).isTrue();
+
+ final var superAttrs = mappedSuperclass.getAttributes();
+
+ // Id attribute should be on the mapped-superclass
+ assertThat( superAttrs.getIdAttributes() )
+ .extracting( JaxbIdImpl::getName )
+ .containsExactly( "id" );
+
+ // Version attribute should be on the mapped-superclass
+ assertThat( superAttrs.getVersion() )
+ .as( "version should be moved to the mapped-superclass" )
+ .isNotNull();
+ assertThat( superAttrs.getVersion().getName() )
+ .isEqualTo( "version" );
+
+ // Basic attribute 'name' should be on the mapped-superclass
+ assertThat( superAttrs.getBasicAttributes() )
+ .extracting( JaxbBasicImpl::getName )
+ .containsExactly( "name" );
+
+ // Many-to-one attribute 'relatedBase' should be on the mapped-superclass
+ assertThat( superAttrs.getManyToOneAttributes() )
+ .extracting( JaxbManyToOneImpl::getName )
+ .containsExactly( "relatedBase" );
+
+ // Unmapped property should be declared as transient
+ assertThat( superAttrs.getTransients() )
+ .extracting( JaxbTransientImpl::getName )
+ .contains( "unmappedProperty" );
+
+ // --- ConcreteEntity assertions ---
+ final JaxbEntityImpl concreteEntity = transformed.getEntities().stream()
+ .filter( e -> "ConcreteEntity".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ // Inherited attributes should NOT be on the entity
+ assertThat( concreteEntity.getAttributes().getIdAttributes() )
+ .as( "Entity should not have id — inherited from mapped-superclass" )
+ .isEmpty();
+ assertThat( concreteEntity.getAttributes().getVersion() )
+ .as( "Entity should not have version — inherited from mapped-superclass" )
+ .isNull();
+ assertThat( concreteEntity.getAttributes().getManyToOneAttributes() )
+ .as( "Entity should not have relatedBase — inherited from mapped-superclass" )
+ .isEmpty();
+
+ // Entity's own attribute should remain
+ assertThat( concreteEntity.getAttributes().getBasicAttributes() )
+ .extracting( JaxbBasicImpl::getName )
+ .containsExactly( "description" );
+
+ // --- AnotherEntity assertions ---
+ final JaxbEntityImpl anotherEntity = transformed.getEntities().stream()
+ .filter( e -> "AnotherEntity".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ // Inherited attributes should NOT be on the entity
+ assertThat( anotherEntity.getAttributes().getIdAttributes() ).isEmpty();
+ assertThat( anotherEntity.getAttributes().getVersion() ).isNull();
+
+ // Entity's own attribute should remain
+ assertThat( anotherEntity.getAttributes().getBasicAttributes() )
+ .extracting( JaxbBasicImpl::getName )
+ .containsExactly( "code" );
+ } );
+ }
+
@Test
@JiraKey( "HHH-20697" )
public void testNonAggregatedCompositeIdColumnsNotUnique(ServiceRegistryScope scope) {
@@ -1236,4 +1409,59 @@ public void testNonAggregatedCompositeIdColumnsNotUnique(ServiceRegistryScope sc
}
} );
}
+
+ @Test
+ @JiraKey( "HHH-20699" )
+ public void testCompositeKeyManyToOneFetchLazy(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml", scope, transformed -> {
+ final JaxbEntityImpl addressEntity = transformed.getEntities().stream()
+ .filter( e -> "Address".equals( e.getClazz() ) )
+ .findFirst()
+ .orElseThrow();
+
+ assertThat( addressEntity.getAttributes().getManyToOneAttributes() )
+ .hasSize( 1 );
+
+ final JaxbManyToOneImpl personManyToOne = addressEntity.getAttributes().getManyToOneAttributes().get( 0 );
+ assertThat( personManyToOne.getName() ).isEqualTo( "person" );
+ assertThat( personManyToOne.isId() ).isTrue();
+ assertThat( personManyToOne.getFetch() )
+ .as( "Composite-id key-many-to-one should have fetch=LAZY" )
+ .isEqualTo( jakarta.persistence.FetchType.LAZY );
+ } );
+ }
+
+ @Test
+ @JiraKey( "HHH-20717" )
+ public void testImportWithoutRenameDefaultsToUnqualifiedClassName(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/import-no-rename/hbm.xml", scope, transformed -> {
+ assertThat( transformed.getHqlImports() ).hasSize( 1 );
+ final JaxbHqlImportImpl hqlImport = (JaxbHqlImportImpl) transformed.getHqlImports().get( 0 );
+ assertThat( hqlImport.getClazz() ).isEqualTo( "Animal" );
+ assertThat( hqlImport.getRename() )
+ .as( "When hbm.xml import has no rename, transformer should default to unqualified class name" )
+ .isEqualTo( "Animal" );
+ } );
+ }
+
+ @Test
+ @JiraKey( "HHH-20703" )
+ public void testCollectionTypeTypedefResolution(ServiceRegistryScope scope) {
+ transformAndVerify( "xml/jaxb/mapping/collection-type-typedef/hbm.xml", scope, transformed -> {
+ final JaxbEntityImpl entity = transformed.getEntities().get( 0 );
+
+ assertThat( entity.getAttributes().getElementCollectionAttributes() ).hasSize( 1 );
+ final var elementCollection = entity.getAttributes().getElementCollectionAttributes().get( 0 );
+ assertThat( elementCollection.getName() ).isEqualTo( "values" );
+ assertThat( elementCollection.getCollectionType() )
+ .as( "collection-type referencing a typedef should be resolved" )
+ .isNotNull();
+ assertThat( elementCollection.getCollectionType().getType() )
+ .as( "collection-type should use the typedef class, not the typedef name" )
+ .isEqualTo( "org.hibernate.orm.test.mapping.collections.custom.parameterized.DefaultableListType" );
+ assertThat( elementCollection.getCollectionType().getParameters() )
+ .as( "collection-type should include typedef parameters" )
+ .hasSize( 1 );
+ } );
+ }
}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/hqlimport/Dog.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/hqlimport/Dog.java
new file mode 100644
index 000000000000..254c82cfc2bb
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/hqlimport/Dog.java
@@ -0,0 +1,11 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.boot.jaxb.mapping.hqlimport;
+
+public class Dog {
+ Long id;
+ String name;
+ String breed;
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AbstractBase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AbstractBase.java
new file mode 100644
index 000000000000..a17ae33751e3
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AbstractBase.java
@@ -0,0 +1,53 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass;
+
+public class AbstractBase {
+ private Long id;
+ private String name;
+ private int version;
+ private AbstractBase relatedBase;
+ private String unmappedProperty;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public void setVersion(int version) {
+ this.version = version;
+ }
+
+ public AbstractBase getRelatedBase() {
+ return relatedBase;
+ }
+
+ public void setRelatedBase(AbstractBase relatedBase) {
+ this.relatedBase = relatedBase;
+ }
+
+ public String getUnmappedProperty() {
+ return unmappedProperty;
+ }
+
+ public void setUnmappedProperty(String unmappedProperty) {
+ this.unmappedProperty = unmappedProperty;
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AnotherEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AnotherEntity.java
new file mode 100644
index 000000000000..0bf29fd9e415
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AnotherEntity.java
@@ -0,0 +1,17 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass;
+
+public class AnotherEntity extends AbstractBase {
+ private String code;
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/ConcreteEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/ConcreteEntity.java
new file mode 100644
index 000000000000..6515b9d1b6c3
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/ConcreteEntity.java
@@ -0,0 +1,17 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass;
+
+public class ConcreteEntity extends AbstractBase {
+ private String description;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml
new file mode 100644
index 000000000000..970fd265d636
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml
new file mode 100644
index 000000000000..78434fe8e9f4
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-type-typedef/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-type-typedef/hbm.xml
new file mode 100644
index 000000000000..86a685cc4934
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/collection-type-typedef/hbm.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Hello
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml
new file mode 100644
index 000000000000..40a5fc428cbc
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/id-class/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/id-class/hbm.xml
new file mode 100644
index 000000000000..ad2ba111fb57
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/id-class/hbm.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/import-no-rename/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/import-no-rename/hbm.xml
new file mode 100644
index 000000000000..31317238d423
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/import-no-rename/hbm.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/on-delete-toone/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/on-delete-toone/hbm.xml
new file mode 100644
index 000000000000..d091de5453d2
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/on-delete-toone/hbm.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hibernate-core/src/test/resources/xml/jaxb/mapping/unmapped-superclass/hbm.xml b/hibernate-core/src/test/resources/xml/jaxb/mapping/unmapped-superclass/hbm.xml
new file mode 100644
index 000000000000..5948d9e75b3b
--- /dev/null
+++ b/hibernate-core/src/test/resources/xml/jaxb/mapping/unmapped-superclass/hbm.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+