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 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..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