From d26383dcefc7bb59e34c4afc7681828d61def9a6 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 16 Jul 2026 16:38:04 +0200 Subject: [PATCH 01/14] HHH-20699 Add test for issue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mapping/HbmTransformationJaxbTests.java | 21 ++++++++++++++++ .../composite-key-many-to-one-fetch/hbm.xml | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml 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..09d43f0cb2c6 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 @@ -1236,4 +1236,25 @@ 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 ); + } ); + } } 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 @@ + + + + + + + + + + + + + + + + + + + + From 3e9971a9327ac149dd46a3c00b0c8e623f3a3c48 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 16 Jul 2026 16:26:53 +0200 Subject: [PATCH 02/14] HHH-20699 HbmXmlTransformer sets EAGER fetch on composite-id key-many-to-one instead of LAZY Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..bd63bb76ec2e 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 @@ -3255,7 +3255,7 @@ private JaxbManyToOneImpl transformCompositeKeyManyToOne( } jaxbKyManyToOne.setOptional( false ); - jaxbKyManyToOne.setFetch( FetchType.EAGER ); + jaxbKyManyToOne.setFetch( FetchType.LAZY ); jaxbKyManyToOne.setFetchMode( JaxbSingularFetchModeImpl.SELECT ); jaxbKyManyToOne.setNotFound( NotFoundAction.EXCEPTION ); From 2f60e698beeeb4a7ace89746570a5403cb9f8625 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 16 Jul 2026 17:13:33 +0200 Subject: [PATCH 03/14] HHH-20703 Add test for issue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mapping/HbmTransformationJaxbTests.java | 21 ++++++++++++++++ .../mapping/collection-type-typedef/hbm.xml | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/collection-type-typedef/hbm.xml 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 09d43f0cb2c6..f99a2fb92bfe 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 @@ -1257,4 +1257,25 @@ public void testCompositeKeyManyToOneFetchLazy(ServiceRegistryScope scope) { .isEqualTo( jakarta.persistence.FetchType.LAZY ); } ); } + + @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/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 + + + + + + + + + + + + From b9d695b5d8e9b667367bb07ce6608a3eecd59825 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 16 Jul 2026 17:24:09 +0200 Subject: [PATCH 04/14] HHH-20703 HbmXmlTransformer does not resolve typedef alias for collection-type attribute Co-Authored-By: Claude Opus 4.6 (1M context) --- .../jaxb/hbm/transform/HbmXmlTransformer.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 bd63bb76ec2e..27a8f43da8ac 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 @@ -2058,7 +2058,20 @@ private void transferCollectionCommonInfo(PluralAttributeInfo source, JaxbPlural 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 ) { From 6a9f183bc89c283e7eb1fd50cb9e6d0941e9c289 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 20 Jul 2026 18:05:34 +0200 Subject: [PATCH 05/14] HHH-20709 Add test for issue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mapping/HbmTransformationJaxbTests.java | 44 +++++++++++++++++++ .../collection-fetch-join-eager/hbm.xml | 28 ++++++++++++ .../collection-fetch-join-lazy/hbm.xml | 28 ++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml 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 f99a2fb92bfe..e115f97e9743 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 @@ -777,6 +777,50 @@ 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-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 ); + } ); + } + private void transformAndVerify( String resourceName, ServiceRegistryScope scope, 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 @@ + + + + + + + + + + + + + + + + + + + + + + From cd054cf0b8fadc8221d7146313d0d3895282f792 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 20 Jul 2026 18:14:38 +0200 Subject: [PATCH 06/14] HHH-20709 HbmXmlTransformer generates fetch-mode=JOIN for lazy collections causing eager initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Since a lazy collection uses a separate SELECT regardless of fetch style, dropping fetch-mode="JOIN" preserves the same runtime behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../boot/jaxb/hbm/transform/HbmXmlTransformer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 27a8f43da8ac..da81afded0f2 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 @@ -2050,8 +2050,17 @@ 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() ); From ab69e1065d86c616c76a71811ae5c0fe1621f9ce Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Tue, 21 Jul 2026 17:40:04 +0200 Subject: [PATCH 07/14] HHH-20711 Add test for issue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mapping/HbmTransformationJaxbTests.java | 22 +++++++++++++++++++ .../xml/jaxb/mapping/id-class/hbm.xml | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/id-class/hbm.xml 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 e115f97e9743..b4741101c5fd 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 @@ -821,6 +821,28 @@ public void testCollectionFetchJoinEagerTransformation(ServiceRegistryScope scop } ); } + @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, 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 @@ + + + + + + + + + + + + + + + + From ba7a1624b3ffd4771be2e5121a4d1064a3591fd6 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Tue, 21 Jul 2026 17:40:27 +0200 Subject: [PATCH 08/14] HHH-20711 HbmXmlTransformer does not support composite-id with id-class Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 da81afded0f2..7deaa5748444 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 @@ -126,6 +126,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; @@ -3391,7 +3392,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( From b4917d5de96efaaee66481b2c452a8c32ae34e2e Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Wed, 22 Jul 2026 11:41:07 +0200 Subject: [PATCH 09/14] HHH-20712 Add test for issue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mapping/HbmTransformationJaxbTests.java | 19 +++++++++++++++++ .../xml/jaxb/mapping/on-delete-toone/hbm.xml | 21 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/on-delete-toone/hbm.xml 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 b4741101c5fd..0a4e1cae6aac 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 @@ -799,6 +799,25 @@ public void testCollectionFetchJoinLazyTransformation(ServiceRegistryScope scope } ); } + @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) { 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 @@ + + + + + + + + + + + + + + + + + + From cd054e325aca70ff66a0f22d23ff62d37807b242 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Wed, 22 Jul 2026 11:41:42 +0200 Subject: [PATCH 10/14] HHH-20712 HbmXmlTransformer does not transfer on-delete="cascade" for many-to-one associations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../boot/jaxb/hbm/transform/HbmXmlTransformer.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 7deaa5748444..f3320c2f29a3 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 @@ -17,6 +17,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; @@ -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; @@ -1904,6 +1906,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() ) ); } From cee733fb619c02696242001ce1453c24f1e57307 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Jul 2026 16:11:56 +0200 Subject: [PATCH 11/14] HHH-20717 Add tes for issue --- .../mapping/HbmTransformationJaxbTests.java | 14 +++++++++++++ .../test/boot/jaxb/mapping/hqlimport/Dog.java | 11 ++++++++++ .../xml/jaxb/mapping/import-no-rename/hbm.xml | 21 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/hqlimport/Dog.java create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/import-no-rename/hbm.xml 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 0a4e1cae6aac..3f699379f570 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,6 +26,7 @@ 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; @@ -1343,6 +1344,19 @@ public void testCompositeKeyManyToOneFetchLazy(ServiceRegistryScope scope) { } ); } + @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) { 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/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 @@ + + + + + + + + + + + + + + + + From b74c63eaaaaf08b32db9208dfb349b4a90f7a0e4 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Jul 2026 16:12:12 +0200 Subject: [PATCH 12/14] HHH-20717 HbmXmlTransformer does not set the required rename attribute for --- .../hibernate/boot/jaxb/hbm/transform/HbmXmlTransformer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f3320c2f29a3..92ff206fdc2c 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 @@ -779,7 +779,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() ) ); } } } From 01763c724863805946945961da54a64d2516f08b Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Jul 2026 15:41:37 +0200 Subject: [PATCH 13/14] HHH-20715 Add tes for issue --- .../mapping/HbmTransformationJaxbTests.java | 87 +++++++++++++++++++ .../unmappedsuperclass/AbstractBase.java | 53 +++++++++++ .../unmappedsuperclass/AnotherEntity.java | 17 ++++ .../unmappedsuperclass/ConcreteEntity.java | 17 ++++ .../jaxb/mapping/unmapped-superclass/hbm.xml | 30 +++++++ 5 files changed, 204 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AbstractBase.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/AnotherEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/boot/jaxb/mapping/unmappedsuperclass/ConcreteEntity.java create mode 100644 hibernate-core/src/test/resources/xml/jaxb/mapping/unmapped-superclass/hbm.xml 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 3f699379f570..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 @@ -30,6 +30,7 @@ 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; @@ -1302,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) { 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/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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + From 1ea13cfa2f9ba981e8f8f5a757e0ee499ea3cc39 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Jul 2026 13:01:56 +0200 Subject: [PATCH 14/14] HHH-20715 HbmXmlTransformer does not generate mapped-superclass for entities with inherited members When an entity maps properties whose Java member is declared on a plain superclass (not a mapped entity), the transformer now generates a for that superclass and moves the inherited attributes from the entity to the mapped-superclass. Unmapped superclass properties are declared as . Co-Authored-By: Claude Opus 4.6 (1M context) --- .../jaxb/hbm/transform/HbmXmlTransformer.java | 309 +++++++++++++++++- .../hbm/transform/TransformationHelper.java | 29 +- 2 files changed, 325 insertions(+), 13 deletions(-) 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 92ff206fdc2c..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; @@ -39,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; @@ -136,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; @@ -369,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... 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 ); 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" ) ) {