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..91273241c305
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+ * }
+ *
+ *
+ * @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
+ * @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/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 a9f7a33168e7..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
@@ -9,6 +9,10 @@
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.models.AnnotationPlacementException;
+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 +87,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 +104,61 @@ 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();
+ }
+
+ 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 ) {
+ 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 extends EmbeddableInstantiator> 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 extends Annotation> 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..dbf557d6371a
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java
@@ -0,0 +1,296 @@
+/*
+ * 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.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
+ @DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.PostCompliant.class})
+ void testCompliantApproach(DomainModelScope modelScope) {
+ verifyModel( modelScope.getEntityBinding( PostCompliant.class ),
+ "posts_compliant",
+ "posts_compliant_secondary",
+ modelScope.getDomainModel() );
+ }
+
+ @Test
+ @ServiceRegistry
+ @DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.Post.class})
+ void testTableNaming(DomainModelScope modelScope) {
+ verifyModel( modelScope.getEntityBinding( Post.class ),
+ "posts",
+ "posts_secondary",
+ modelScope.getDomainModel() );
+ }
+
+ void verifyModel(
+ PersistentClass entityBinding,
+ String primaryTableName,
+ String secondaryTableName,
+ MetadataImplementor domainModel) {
+ final Property nameProperty = entityBinding.getProperty( "name" );
+ assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTableName );
+
+ final Property primaryTagProperty = entityBinding.getProperty( "tag" );
+ 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
+ @ServiceRegistry
+ @DomainModel(annotatedClasses = {
+ EmbeddedTableTests.Nested.class,
+ EmbeddedTableTests.Container.class,
+ EmbeddedTableTests.TopContainer.class
+ })
+ void testNestedModel(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" );
+
+ 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) {
+ 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 );
+ } );
+ }
+
+ @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;
+ Instant added;
+ }
+
+ @Entity(name="Post")
+ @Table(name="posts")
+ @SecondaryTable(name="posts_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk"))
+ 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", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk"))
+ 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
+ Nested nested;
+ }
+
+ @Entity(name="TopContainer")
+ @Table(name="top")
+ @SecondaryTable(name="supp", pkJoinColumns = @PrimaryKeyJoinColumn(name = "top_fk"))
+ public static class TopContainer {
+ @Id
+ private Integer id;
+ private String name;
+ @Embedded
+ @EmbeddedTable("supp")
+ private Container subContainer;
+
+ @ElementCollection
+ @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;
+ }
+
}
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