From 69e31d0936772a863d5bab81b700584735fdcfe8 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 2 Oct 2025 16:33:28 -0600 Subject: [PATCH 1/2] HHH-19257 - Introduce @EmbeddedTable --- .../hibernate/annotations/EmbeddedTable.java | 46 ++++++ .../internal/AbstractPropertyHolder.java | 2 +- .../internal/ComponentPropertyHolder.java | 40 +++++ .../boot/model/internal/EmbeddableBinder.java | 10 +- .../boot/model/internal/PropertyBinder.java | 7 + .../boot/models/HibernateAnnotations.java | 4 + .../internal/EmbeddedTableAnnotation.java | 57 +++++++ .../java/org/hibernate/mapping/Component.java | 19 +++ .../embeddable/table/EmbeddedTableTests.java | 152 ++++++++++++++++++ whats-new.adoc | 49 ++++++ 10 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java create mode 100644 hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/EmbeddedTableAnnotation.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java new file mode 100644 index 000000000000..7b12b65fea24 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import org.hibernate.Incubating; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Allows an easier mechanism to declare the table to which an embedded value + * maps compared to the Jakarta Persistence compliant mechanism requiring + * multiple {@link jakarta.persistence.AttributeOverride} + * and {@link jakarta.persistence.AssociationOverride} annotations. + *
+ *  @Entity
+ *  @Table(name="primary")
+ *  @SecondaryTable(name="secondary")
+ *  class Person {
+ *  	...
+ *  	@Embedded
+ *  	@EmbeddedTable("secondary")
+ *  	Address address;
+ *  }
+ * 
+ * + * @see EmbeddedColumnNaming + * + * @since 7.2 + * @author Steve Ebersole + */ +@Target({METHOD, FIELD}) +@Retention(RUNTIME) +@Incubating +public @interface EmbeddedTable { + /** + * The name of the table in which the embedded value is stored. + */ + String value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java index 04877d3070a6..473e2775e364 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java @@ -53,7 +53,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { private final String path; protected final AbstractPropertyHolder parent; - private final MetadataBuildingContext context; + protected final MetadataBuildingContext context; private Boolean isInIdClass; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java index a9f7a33168e7..da8127bd92b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java @@ -9,6 +9,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AnnotationException; +import org.hibernate.annotations.EmbeddedTable; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.mapping.AggregateColumn; @@ -83,6 +86,8 @@ public ComponentPropertyHolder( this.component = component; this.inheritanceStatePerClass = inheritanceStatePerClass; + applyExplicitTableName( component, inferredData, parent, context ); + isOrWithinEmbeddedId = parent.isOrWithinEmbeddedId() || embeddedMemberDetails != null && hasIdAnnotation( embeddedMemberDetails ); isWithinElementCollection = parent.isWithinElementCollection() @@ -98,6 +103,41 @@ public ComponentPropertyHolder( } } + /** + * Apply the explicit {@link EmbeddedTable} if there is one and if its + * appropriate for the context (the type of {@code container}). + * + * @param component The (in-flight) component mapping details. + * @param propertyData Details about the property defining this component. + * @param container The container for this component. + */ + public static void applyExplicitTableName( + Component component, + PropertyData propertyData, + PropertyHolder container, + MetadataBuildingContext buildingContext) { + Table tableToUse = container.getTable(); + boolean wasExplicit = false; + if ( container instanceof ComponentPropertyHolder componentPropertyHolder ) { + wasExplicit = componentPropertyHolder.getComponent().wasTableExplicitlyDefined(); + } + + // we only allow this when done for an embedded on an entity or mapped-superclass + if ( propertyData.getAttributeMember() != null && container instanceof ClassPropertyHolder ) { + final EmbeddedTable embeddedTableAnn = propertyData.getAttributeMember().getDirectAnnotationUsage( EmbeddedTable.class ); + if ( embeddedTableAnn != null ) { + final Identifier tableNameIdentifier = buildingContext.getObjectNameNormalizer().normalizeIdentifierQuoting( embeddedTableAnn.value() ); + final InFlightMetadataCollector.EntityTableXref entityTableXref = buildingContext + .getMetadataCollector() + .getEntityTableXref( container.getEntityName() ); + tableToUse = entityTableXref.resolveTable( tableNameIdentifier ); + wasExplicit = true; + } + } + + component.setTable( tableToUse, wasExplicit ); + } + /** * Access to the underlying component */ diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java index d5d4e249ca43..99d6e1917c3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java @@ -951,16 +951,16 @@ private static String canonicalize(String typeName) { static Component createEmbeddable( PropertyHolder propertyHolder, PropertyData inferredData, - boolean isComponentEmbedded, + boolean isNonAggregated, boolean isIdentifierMapper, Class customInstantiatorImpl, MetadataBuildingContext context) { final var embeddable = new Component( context, propertyHolder.getPersistentClass() ); - embeddable.setEmbedded( isComponentEmbedded ); - //yuk - embeddable.setTable( propertyHolder.getTable() ); + embeddable.setEmbedded( isNonAggregated ); + ComponentPropertyHolder.applyExplicitTableName( embeddable, inferredData, propertyHolder, context ); + if ( isIdentifierMapper - || isComponentEmbedded && inferredData.getPropertyName() == null ) { + || isNonAggregated && inferredData.getPropertyName() == null ) { embeddable.setComponentClassName( embeddable.getOwner().getClassName() ); } else { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index 32c51ed9bdac..e9c32b3564cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -269,6 +269,13 @@ private Property makePropertyAndValue() { basicValueBinder.setReferencedEntityName( referencedEntityName ); basicValueBinder.setAccessType( accessType ); + if ( holder instanceof ComponentPropertyHolder embeddableTypedContainer ) { + final Component component = embeddableTypedContainer.getComponent(); + if ( component.wasTableExplicitlyDefined() ) { + basicValueBinder.setTable( component.getTable() ); + } + } + value = basicValueBinder.make(); return makeProperty(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java index ccba0506939a..7c343f3bac4b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java @@ -248,6 +248,10 @@ public interface HibernateAnnotations { EmbeddedColumnNaming.class, EmbeddedColumnNamingAnnotation.class ); + OrmAnnotationDescriptor EMBEDDED_TABLE = new OrmAnnotationDescriptor<>( + EmbeddedTable.class, + EmbeddedTableAnnotation.class + ); OrmAnnotationDescriptor FETCH = new OrmAnnotationDescriptor<>( Fetch.class, FetchAnnotation.class diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/EmbeddedTableAnnotation.java b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/EmbeddedTableAnnotation.java new file mode 100644 index 000000000000..90178289ea26 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/EmbeddedTableAnnotation.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.models.annotations.internal; + +import org.hibernate.annotations.EmbeddedTable; +import org.hibernate.models.spi.ModelsContext; + +import java.lang.annotation.Annotation; +import java.util.Map; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings({ "ClassExplicitlyAnnotation", "unused" }) +public class EmbeddedTableAnnotation implements EmbeddedTable { + private String value; + + /** + * Used in creating dynamic annotation instances (e.g. from XML) + */ + public EmbeddedTableAnnotation(ModelsContext modelContext) { + } + + /** + * Used in creating annotation instances from JDK variant + */ + public EmbeddedTableAnnotation( + EmbeddedTable annotation, + ModelsContext modelContext) { + this.value = annotation.value(); + } + + /** + * Used in creating annotation instances from Jandex variant + */ + public EmbeddedTableAnnotation( + Map attributeValues, + ModelsContext modelContext) { + this.value = (String) attributeValues.get( "value" ); + } + + @Override + public Class annotationType() { + return EmbeddedTable.class; + } + + @Override + public String value() { + return value; + } + + public void value(String value) { + this.value = value; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index 33462c3bcf2b..f77bcd806b85 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -100,6 +100,8 @@ public class Component extends SimpleValue implements AttributeContainer, MetaAt private transient Boolean simpleRecord; private String columnNamingPattern; + private boolean tableWasExplicit; + public Component(MetadataBuildingContext metadata, PersistentClass owner) throws MappingException { this( metadata, owner.getTable(), owner ); } @@ -158,6 +160,23 @@ public List getProperties() { return properties; } + public void setTable(Table table) { + if ( !tableWasExplicit ) { + super.setTable( table ); + } + + // otherwise, ignore it... + } + + public void setTable(Table table, boolean wasExplicit) { + super.setTable( table ); + tableWasExplicit = wasExplicit; + } + + public boolean wasTableExplicitlyDefined() { + return tableWasExplicit; + } + public void addProperty(Property p, ClassDetails declaringClass) { properties.add( p ); if ( isPolymorphic() && declaringClass != null ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java new file mode 100644 index 000000000000..e82ae01e0d3d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.embeddable.table; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import org.hibernate.annotations.EmbeddedTable; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@ServiceRegistry +public class EmbeddedTableTests { + @Test + @ServiceRegistry + @DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.PostCompliant.class}) + void testCompliantApproach(DomainModelScope modelScope) { + verifyModel( modelScope.getEntityBinding( PostCompliant.class ), + "posts_compliant", + "posts_compliant_secondary" ); + } + + @Test + @ServiceRegistry + @DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.Post.class}) + void testTableNaming(DomainModelScope modelScope) { + verifyModel( modelScope.getEntityBinding( Post.class ), + "posts", + "posts_secondary" ); + } + + void verifyModel(PersistentClass entityBinding, String primaryTable, String secondaryTable) { + final Property nameProperty = entityBinding.getProperty( "name" ); + assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTable ); + + final Property primaryTagProperty = entityBinding.getProperty( "tag" ); + assertThat( primaryTagProperty.getValue().getTable().getName() ).isEqualTo( secondaryTable ); + } + + @Test + @ServiceRegistry + @DomainModel(annotatedClasses = { + EmbeddedTableTests.Nested.class, + EmbeddedTableTests.Container.class, + EmbeddedTableTests.TopContainer.class + }) + void testNestedUsage(DomainModelScope modelScope) { + final PersistentClass entityBinding = modelScope.getEntityBinding( TopContainer.class ); + + final Property subContainerProp = entityBinding.getProperty( "subContainer" ); + checkContainerComponent( (Component) subContainerProp.getValue(), "supp" ); + + final Property subContainersProp = entityBinding.getProperty( "subContainers" ); + final Collection containersPropValue = (Collection) subContainersProp.getValue(); + checkContainerComponent( (Component) containersPropValue.getElement(), "sub_containers" ); + } + + private void checkContainerComponent(Component containerComponent, String tableName) { + assertThat( containerComponent.getTable().getName() ).isEqualTo( tableName ); + assertThat( containerComponent.getPropertySpan() ).isEqualTo( 1 ); + final Property nestedProp = containerComponent.getProperty( "nested" ); + final Component nestedComponent = (Component) nestedProp.getValue(); + nestedComponent.getProperties().forEach( (subProp) -> { + assertThat( subProp.getValue().getTable().getName() ).isEqualTo( tableName ); + } ); + } + + @Embeddable + public static class Tag { + String text; + Instant added; + } + + @Entity(name="Post") + @Table(name="posts") + @SecondaryTable(name="posts_secondary") + public static class Post { + @Id + private Integer id; + private String name; + @Embedded + @EmbeddedTable("posts_secondary") + private Tag tag; + } + + @Entity(name="PostCompliant") + @Table(name="posts_compliant") + @SecondaryTable(name="posts_compliant_secondary") + public static class PostCompliant { + @Id + private Integer id; + private String name; + @Embedded + @AttributeOverride(name="text", column = @Column(table = "posts_compliant_secondary") ) + @AttributeOverride(name="added", column = @Column(table = "posts_compliant_secondary") ) + private Tag tag; + } + + @Embeddable + public static class Nested { + String thing1; + String thing2; + } + + @Embeddable + public static class Container { + @Embedded + @EmbeddedTable("should_be_ignored") // or maybe this should be an error? same for element-collections of embeddables + Nested nested; + } + + @Entity(name="TopContainer") + @Table(name="top") + @SecondaryTable(name="supp") + public static class TopContainer { + @Id + private Integer id; + private String name; + @Embedded + @EmbeddedTable("supp") + private Container subContainer; + + @ElementCollection + @CollectionTable(name = "sub_containers") + @EmbeddedTable("supp") + private Set subContainers; + } +} diff --git a/whats-new.adoc b/whats-new.adoc index e6bb7886734c..44eddb0010db 100644 --- a/whats-new.adoc +++ b/whats-new.adoc @@ -11,6 +11,55 @@ Describes the new features and capabilities added to Hibernate ORM in {version}. IMPORTANT: If migrating from earlier versions, be sure to also check out the link:{migrationGuide}[Migration Guide] for discussion of impactful changes. +[[embedded-table]] +== @EmbeddedTable + +The Jakarta Persistence compliant way to specify the table to which an embedded value maps is tedious, at best, requiring us of multiple `@AttributeOverride` and/or `@AssociationOverride` annotations - + +==== +[source,java] +---- +@Entity +@Table(name="primary") +@SecondaryTable(name="secondary") +class Person { + ... + + @Embedded + @AttributeOverride(name="street", + column=@Column(table="secondary")) + @AttributeOverride(name="city", + column=@Column(table="secondary")) + @AttributeOverride(name="state", + column=@Column(table="secondary")) + @AttributeOverride(name="zip", + column=@Column(table="secondary")) + Address address; +} +---- +==== + +Hibernate now provides the `EmbeddedTable` annotation to help make this easier - + +==== +[source,java] +---- +@Entity +@Table(name="primary") +@SecondaryTable(name="secondary") +class Person { + ... + + @Embedded + @EmbeddedTable("secondary") + Address address; +} +---- +==== + +The annotation is only legal on top-level embedded. Placement on nested embedded values will be ignored. + + [[child-stateless-sessions]] == Child StatelessSession From 32aab4a0a6fd5b71d3ab7f6b1525e2034ddefc70 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 2 Oct 2025 17:49:48 -0600 Subject: [PATCH 2/2] HHH-19257 - Introduce @EmbeddedTable --- .../hibernate/annotations/EmbeddedTable.java | 3 + .../boot/model/internal/CollectionBinder.java | 10 +- .../internal/ComponentPropertyHolder.java | 23 ++- .../embeddable/table/EmbeddedTableTests.java | 168 ++++++++++++++++-- .../orm/junit/ServiceRegistryScope.java | 13 ++ 5 files changed, 203 insertions(+), 14 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java index 7b12b65fea24..91273241c305 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java @@ -30,6 +30,9 @@ * } * * + * @apiNote Only supported for the embedded defined on an entity or mapped-superclass; all other (mis)uses + * will lead to a {@linkplain org.hibernate.boot.models.AnnotationPlacementException}. + * * @see EmbeddedColumnNaming * * @since 7.2 diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index fe322fa24793..00f1c3977530 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -20,6 +20,7 @@ import org.hibernate.MappingException; import org.hibernate.annotations.*; import org.hibernate.boot.model.IdentifierGeneratorDefinition; +import org.hibernate.boot.models.AnnotationPlacementException; import org.hibernate.boot.models.JpaAnnotations; import org.hibernate.boot.models.annotations.internal.MapKeyColumnJpaAnnotation; import org.hibernate.boot.spi.AccessType; @@ -1052,10 +1053,17 @@ private void setDeclaringClass(ClassDetails declaringClass) { } private void bind() { + if ( property != null ) { + final EmbeddedTable misplaced = property.getDirectAnnotationUsage( EmbeddedTable.class ); + if ( misplaced != null ) { + // not allowed + throw new AnnotationPlacementException( "@EmbeddedTable only supported for use on entity or mapped-superclass" ); + } + } collection = createCollection( propertyHolder.getPersistentClass() ); final String role = qualify( propertyHolder.getPath(), propertyName ); if ( BOOT_LOGGER.isTraceEnabled() ) { -BOOT_LOGGER.bindingCollectionRole( role ); + BOOT_LOGGER.bindingCollectionRole( role ); } collection.setRole( role ); collection.setMappedByProperty( mappedBy ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java index da8127bd92b3..86d936ec68fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java @@ -11,6 +11,7 @@ import org.hibernate.AnnotationException; import org.hibernate.annotations.EmbeddedTable; import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.models.AnnotationPlacementException; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; @@ -122,7 +123,27 @@ public static void applyExplicitTableName( wasExplicit = componentPropertyHolder.getComponent().wasTableExplicitlyDefined(); } - // we only allow this when done for an embedded on an entity or mapped-superclass + if ( propertyData.getAttributeMember() != null ) { + final EmbeddedTable embeddedTableAnn = propertyData.getAttributeMember() + .getDirectAnnotationUsage( EmbeddedTable.class ); + // we only allow this when done for an embedded on an entity or mapped-superclass + if ( container instanceof ClassPropertyHolder ) { + if ( embeddedTableAnn != null ) { + final Identifier tableNameIdentifier = buildingContext.getObjectNameNormalizer().normalizeIdentifierQuoting( embeddedTableAnn.value() ); + final InFlightMetadataCollector.EntityTableXref entityTableXref = buildingContext + .getMetadataCollector() + .getEntityTableXref( container.getEntityName() ); + tableToUse = entityTableXref.resolveTable( tableNameIdentifier ); + wasExplicit = true; + } + } + else { + if ( embeddedTableAnn != null ) { + // not allowed + throw new AnnotationPlacementException( "@EmbeddedTable only supported for use on entity or mapped-superclass" ); + } + } + } if ( propertyData.getAttributeMember() != null && container instanceof ClassPropertyHolder ) { final EmbeddedTable embeddedTableAnn = propertyData.getAttributeMember().getDirectAnnotationUsage( EmbeddedTable.class ); if ( embeddedTableAnn != null ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java index e82ae01e0d3d..dbf557d6371a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java @@ -12,28 +12,43 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.PrimaryKeyJoinColumn; import jakarta.persistence.SecondaryTable; import jakarta.persistence.Table; import org.hibernate.annotations.EmbeddedTable; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.boot.models.AnnotationPlacementException; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.jpa.HibernatePersistenceConfiguration; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Component; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @author Steve Ebersole */ @SuppressWarnings("JUnitMalformedDeclaration") @ServiceRegistry +@RequiresDialect(value = H2Dialect.class, comment = "The underlying database has no effect on this, so just run on the default" ) public class EmbeddedTableTests { @Test @ServiceRegistry @@ -41,7 +56,8 @@ public class EmbeddedTableTests { void testCompliantApproach(DomainModelScope modelScope) { verifyModel( modelScope.getEntityBinding( PostCompliant.class ), "posts_compliant", - "posts_compliant_secondary" ); + "posts_compliant_secondary", + modelScope.getDomainModel() ); } @Test @@ -50,15 +66,36 @@ void testCompliantApproach(DomainModelScope modelScope) { void testTableNaming(DomainModelScope modelScope) { verifyModel( modelScope.getEntityBinding( Post.class ), "posts", - "posts_secondary" ); + "posts_secondary", + modelScope.getDomainModel() ); } - void verifyModel(PersistentClass entityBinding, String primaryTable, String secondaryTable) { + void verifyModel( + PersistentClass entityBinding, + String primaryTableName, + String secondaryTableName, + MetadataImplementor domainModel) { final Property nameProperty = entityBinding.getProperty( "name" ); - assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTable ); + assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTableName ); final Property primaryTagProperty = entityBinding.getProperty( "tag" ); - assertThat( primaryTagProperty.getValue().getTable().getName() ).isEqualTo( secondaryTable ); + assertThat( primaryTagProperty.getValue().getTable().getName() ).isEqualTo( secondaryTableName ); + + final Namespace dbNamespace = domainModel.getDatabase().getDefaultNamespace(); + + // id, name + final org.hibernate.mapping.Table primaryTable = dbNamespace.locateTable( + Identifier.toIdentifier( primaryTableName ) ); + assertThat( primaryTable.getColumns() ).hasSize( 2 ); + assertThat( primaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) ) + .containsExactlyInAnyOrder( "id", "name" ); + + // text, added + final org.hibernate.mapping.Table secondaryTable = dbNamespace.locateTable( + Identifier.toIdentifier( secondaryTableName ) ); + assertThat( secondaryTable.getColumns() ).hasSize( 3 ); + assertThat( secondaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) ) + .containsExactlyInAnyOrder( "text", "added", "post_fk" ); } @Test @@ -68,7 +105,7 @@ void verifyModel(PersistentClass entityBinding, String primaryTable, String seco EmbeddedTableTests.Container.class, EmbeddedTableTests.TopContainer.class }) - void testNestedUsage(DomainModelScope modelScope) { + void testNestedModel(DomainModelScope modelScope) { final PersistentClass entityBinding = modelScope.getEntityBinding( TopContainer.class ); final Property subContainerProp = entityBinding.getProperty( "subContainer" ); @@ -77,6 +114,29 @@ void testNestedUsage(DomainModelScope modelScope) { final Property subContainersProp = entityBinding.getProperty( "subContainers" ); final Collection containersPropValue = (Collection) subContainersProp.getValue(); checkContainerComponent( (Component) containersPropValue.getElement(), "sub_containers" ); + + final Namespace dbNamespace = modelScope.getDomainModel().getDatabase().getDefaultNamespace(); + + // id, name + final org.hibernate.mapping.Table primaryTable = dbNamespace.locateTable( + Identifier.toIdentifier( "top" ) ); + assertThat( primaryTable.getColumns() ).hasSize( 2 ); + assertThat( primaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) ) + .containsExactlyInAnyOrder( "id", "name" ); + + // thing1, thing2, top_fk + final org.hibernate.mapping.Table secondaryTable = dbNamespace.locateTable( + Identifier.toIdentifier( "supp" ) ); + assertThat( secondaryTable.getColumns() ).hasSize( 3 ); + assertThat( secondaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) ) + .containsExactlyInAnyOrder( "thing1", "thing2", "top_fk" ); + + // thing1, thing2, top_fk + final org.hibernate.mapping.Table collectionTable = dbNamespace.locateTable( + Identifier.toIdentifier( "sub_containers" ) ); + assertThat( collectionTable.getColumns() ).hasSize( 3 ); + assertThat( collectionTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) ) + .containsExactlyInAnyOrder( "thing1", "thing2", "top_fk" ); } private void checkContainerComponent(Component containerComponent, String tableName) { @@ -89,6 +149,49 @@ private void checkContainerComponent(Component containerComponent, String tableN } ); } + @Test + @ServiceRegistry + @DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.Post.class}) + @SessionFactory(useCollectingStatementInspector = true) + void testDatabase(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + sqlCollector.clear(); + + factoryScope.inTransaction( (session) -> { + // NOTE: ... from posts p1_0 left join posts_secondary p1_1 ... + session.createSelectionQuery( "from Post", Post.class ).list(); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ) + .contains( "p1_0.id", "p1_0.name", "p1_1.added", "p1_1.text" ); + } ); + } + + @Test + @ServiceRegistry + void testBadNestedPlacement(ServiceRegistryScope registryScope) { + final HibernatePersistenceConfiguration persistenceConfiguration = registryScope + .createPersistenceConfiguration( "bad-nested" ) + .managedClasses( Bottom.class, BadMiddle.class, BadNesterEntity.class ); + try ( var sf = persistenceConfiguration.createEntityManagerFactory() ) { + fail( "Should have failed with AnnotationPlacementException" ); + } + catch (AnnotationPlacementException expected) { + } + } + + @Test + @ServiceRegistry + void testBadCollectionPlacement(ServiceRegistryScope registryScope) { + final HibernatePersistenceConfiguration persistenceConfiguration = registryScope + .createPersistenceConfiguration( "bad-nested" ) + .managedClasses( Bottom.class, Middle.class, BadCollectionEntity.class ); + try ( var sf = persistenceConfiguration.createEntityManagerFactory() ) { + fail( "Should have failed with AnnotationPlacementException" ); + } + catch (AnnotationPlacementException expected) { + } + } + @Embeddable public static class Tag { String text; @@ -97,7 +200,7 @@ public static class Tag { @Entity(name="Post") @Table(name="posts") - @SecondaryTable(name="posts_secondary") + @SecondaryTable(name="posts_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk")) public static class Post { @Id private Integer id; @@ -109,7 +212,7 @@ public static class Post { @Entity(name="PostCompliant") @Table(name="posts_compliant") - @SecondaryTable(name="posts_compliant_secondary") + @SecondaryTable(name="posts_compliant_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk")) public static class PostCompliant { @Id private Integer id; @@ -129,13 +232,12 @@ public static class Nested { @Embeddable public static class Container { @Embedded - @EmbeddedTable("should_be_ignored") // or maybe this should be an error? same for element-collections of embeddables Nested nested; } @Entity(name="TopContainer") @Table(name="top") - @SecondaryTable(name="supp") + @SecondaryTable(name="supp", pkJoinColumns = @PrimaryKeyJoinColumn(name = "top_fk")) public static class TopContainer { @Id private Integer id; @@ -145,8 +247,50 @@ public static class TopContainer { private Container subContainer; @ElementCollection - @CollectionTable(name = "sub_containers") - @EmbeddedTable("supp") + @CollectionTable(name = "sub_containers", joinColumns = @JoinColumn(name = "top_fk")) private Set subContainers; } + + @Embeddable + public static class Bottom { + private String kind; + private Instant whenReached; + } + + @Embeddable + public static class BadMiddle { + @Embedded + @EmbeddedTable("secondary") + private Bottom bottom; + } + + @Embeddable + public static class Middle { + @Embedded + private Bottom bottom; + } + + @Entity(name="BadNesterEntity") + @Table(name="primary") + @SecondaryTable(name="secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "primary_fk")) + public static class BadNesterEntity { + @Id + private Integer id; + private String name; + @Embedded + @EmbeddedTable("secondary") + BadMiddle badMiddle; + } + + @Entity(name="BadNesterEntity") + @Table(name="primary") + @SecondaryTable(name="secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "primary_fk")) + public static class BadCollectionEntity { + @Id + private Integer id; + private String name; + @ElementCollection + @EmbeddedTable("secondary") + Set middles; + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/ServiceRegistryScope.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/ServiceRegistryScope.java index 179cbb6beea9..90caaaa14fc2 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/ServiceRegistryScope.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/ServiceRegistryScope.java @@ -9,6 +9,8 @@ import java.util.function.Supplier; import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.jpa.HibernatePersistenceConfiguration; import org.hibernate.service.Service; /** @@ -50,4 +52,15 @@ default R fromService(Class role, Function action return action.apply( service ); } + + default HibernatePersistenceConfiguration createPersistenceConfiguration(String persistenceUnitName) { + final HibernatePersistenceConfiguration configuration = new HibernatePersistenceConfiguration( persistenceUnitName ); + final StandardServiceRegistry registry = getRegistry(); + + final ConfigurationService configurationService = registry.requireService( ConfigurationService.class ); + configuration.properties( configurationService.getSettings() ); + + return configuration; + } + }