Skip to content

Commit 32aab4a

Browse files
committed
HHH-19257 - Introduce @EmbeddedTable
1 parent 69e31d0 commit 32aab4a

File tree

5 files changed

+203
-14
lines changed

5 files changed

+203
-14
lines changed

hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedTable.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
* }
3131
* </pre>
3232
*
33+
* @apiNote Only supported for the embedded defined on an entity or mapped-superclass; all other (mis)uses
34+
* will lead to a {@linkplain org.hibernate.boot.models.AnnotationPlacementException}.
35+
*
3336
* @see EmbeddedColumnNaming
3437
*
3538
* @since 7.2

hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.hibernate.MappingException;
2121
import org.hibernate.annotations.*;
2222
import org.hibernate.boot.model.IdentifierGeneratorDefinition;
23+
import org.hibernate.boot.models.AnnotationPlacementException;
2324
import org.hibernate.boot.models.JpaAnnotations;
2425
import org.hibernate.boot.models.annotations.internal.MapKeyColumnJpaAnnotation;
2526
import org.hibernate.boot.spi.AccessType;
@@ -1052,10 +1053,17 @@ private void setDeclaringClass(ClassDetails declaringClass) {
10521053
}
10531054

10541055
private void bind() {
1056+
if ( property != null ) {
1057+
final EmbeddedTable misplaced = property.getDirectAnnotationUsage( EmbeddedTable.class );
1058+
if ( misplaced != null ) {
1059+
// not allowed
1060+
throw new AnnotationPlacementException( "@EmbeddedTable only supported for use on entity or mapped-superclass" );
1061+
}
1062+
}
10551063
collection = createCollection( propertyHolder.getPersistentClass() );
10561064
final String role = qualify( propertyHolder.getPath(), propertyName );
10571065
if ( BOOT_LOGGER.isTraceEnabled() ) {
1058-
BOOT_LOGGER.bindingCollectionRole( role );
1066+
BOOT_LOGGER.bindingCollectionRole( role );
10591067
}
10601068
collection.setRole( role );
10611069
collection.setMappedByProperty( mappedBy );

hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.hibernate.AnnotationException;
1212
import org.hibernate.annotations.EmbeddedTable;
1313
import org.hibernate.boot.model.naming.Identifier;
14+
import org.hibernate.boot.models.AnnotationPlacementException;
1415
import org.hibernate.boot.spi.InFlightMetadataCollector;
1516
import org.hibernate.boot.spi.MetadataBuildingContext;
1617
import org.hibernate.boot.spi.PropertyData;
@@ -122,7 +123,27 @@ public static void applyExplicitTableName(
122123
wasExplicit = componentPropertyHolder.getComponent().wasTableExplicitlyDefined();
123124
}
124125

125-
// we only allow this when done for an embedded on an entity or mapped-superclass
126+
if ( propertyData.getAttributeMember() != null ) {
127+
final EmbeddedTable embeddedTableAnn = propertyData.getAttributeMember()
128+
.getDirectAnnotationUsage( EmbeddedTable.class );
129+
// we only allow this when done for an embedded on an entity or mapped-superclass
130+
if ( container instanceof ClassPropertyHolder ) {
131+
if ( embeddedTableAnn != null ) {
132+
final Identifier tableNameIdentifier = buildingContext.getObjectNameNormalizer().normalizeIdentifierQuoting( embeddedTableAnn.value() );
133+
final InFlightMetadataCollector.EntityTableXref entityTableXref = buildingContext
134+
.getMetadataCollector()
135+
.getEntityTableXref( container.getEntityName() );
136+
tableToUse = entityTableXref.resolveTable( tableNameIdentifier );
137+
wasExplicit = true;
138+
}
139+
}
140+
else {
141+
if ( embeddedTableAnn != null ) {
142+
// not allowed
143+
throw new AnnotationPlacementException( "@EmbeddedTable only supported for use on entity or mapped-superclass" );
144+
}
145+
}
146+
}
126147
if ( propertyData.getAttributeMember() != null && container instanceof ClassPropertyHolder ) {
127148
final EmbeddedTable embeddedTableAnn = propertyData.getAttributeMember().getDirectAnnotationUsage( EmbeddedTable.class );
128149
if ( embeddedTableAnn != null ) {

hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/table/EmbeddedTableTests.java

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,52 @@
1212
import jakarta.persistence.Embedded;
1313
import jakarta.persistence.Entity;
1414
import jakarta.persistence.Id;
15+
import jakarta.persistence.JoinColumn;
16+
import jakarta.persistence.PrimaryKeyJoinColumn;
1517
import jakarta.persistence.SecondaryTable;
1618
import jakarta.persistence.Table;
1719
import org.hibernate.annotations.EmbeddedTable;
20+
import org.hibernate.boot.model.naming.Identifier;
21+
import org.hibernate.boot.model.relational.Namespace;
22+
import org.hibernate.boot.models.AnnotationPlacementException;
23+
import org.hibernate.boot.spi.MetadataImplementor;
24+
import org.hibernate.dialect.H2Dialect;
25+
import org.hibernate.jpa.HibernatePersistenceConfiguration;
1826
import org.hibernate.mapping.Collection;
1927
import org.hibernate.mapping.Component;
2028
import org.hibernate.mapping.PersistentClass;
2129
import org.hibernate.mapping.Property;
30+
import org.hibernate.testing.jdbc.SQLStatementInspector;
2231
import org.hibernate.testing.orm.junit.DomainModel;
2332
import org.hibernate.testing.orm.junit.DomainModelScope;
33+
import org.hibernate.testing.orm.junit.RequiresDialect;
2434
import org.hibernate.testing.orm.junit.ServiceRegistry;
35+
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
36+
import org.hibernate.testing.orm.junit.SessionFactory;
37+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
2538
import org.junit.jupiter.api.Test;
2639

2740
import java.time.Instant;
2841
import java.util.Set;
2942

3043
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.assertj.core.api.Assertions.fail;
3145

3246
/**
3347
* @author Steve Ebersole
3448
*/
3549
@SuppressWarnings("JUnitMalformedDeclaration")
3650
@ServiceRegistry
51+
@RequiresDialect(value = H2Dialect.class, comment = "The underlying database has no effect on this, so just run on the default" )
3752
public class EmbeddedTableTests {
3853
@Test
3954
@ServiceRegistry
4055
@DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.PostCompliant.class})
4156
void testCompliantApproach(DomainModelScope modelScope) {
4257
verifyModel( modelScope.getEntityBinding( PostCompliant.class ),
4358
"posts_compliant",
44-
"posts_compliant_secondary" );
59+
"posts_compliant_secondary",
60+
modelScope.getDomainModel() );
4561
}
4662

4763
@Test
@@ -50,15 +66,36 @@ void testCompliantApproach(DomainModelScope modelScope) {
5066
void testTableNaming(DomainModelScope modelScope) {
5167
verifyModel( modelScope.getEntityBinding( Post.class ),
5268
"posts",
53-
"posts_secondary" );
69+
"posts_secondary",
70+
modelScope.getDomainModel() );
5471
}
5572

56-
void verifyModel(PersistentClass entityBinding, String primaryTable, String secondaryTable) {
73+
void verifyModel(
74+
PersistentClass entityBinding,
75+
String primaryTableName,
76+
String secondaryTableName,
77+
MetadataImplementor domainModel) {
5778
final Property nameProperty = entityBinding.getProperty( "name" );
58-
assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTable );
79+
assertThat( nameProperty.getValue().getTable().getName() ).isEqualTo( primaryTableName );
5980

6081
final Property primaryTagProperty = entityBinding.getProperty( "tag" );
61-
assertThat( primaryTagProperty.getValue().getTable().getName() ).isEqualTo( secondaryTable );
82+
assertThat( primaryTagProperty.getValue().getTable().getName() ).isEqualTo( secondaryTableName );
83+
84+
final Namespace dbNamespace = domainModel.getDatabase().getDefaultNamespace();
85+
86+
// id, name
87+
final org.hibernate.mapping.Table primaryTable = dbNamespace.locateTable(
88+
Identifier.toIdentifier( primaryTableName ) );
89+
assertThat( primaryTable.getColumns() ).hasSize( 2 );
90+
assertThat( primaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) )
91+
.containsExactlyInAnyOrder( "id", "name" );
92+
93+
// text, added
94+
final org.hibernate.mapping.Table secondaryTable = dbNamespace.locateTable(
95+
Identifier.toIdentifier( secondaryTableName ) );
96+
assertThat( secondaryTable.getColumns() ).hasSize( 3 );
97+
assertThat( secondaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) )
98+
.containsExactlyInAnyOrder( "text", "added", "post_fk" );
6299
}
63100

64101
@Test
@@ -68,7 +105,7 @@ void verifyModel(PersistentClass entityBinding, String primaryTable, String seco
68105
EmbeddedTableTests.Container.class,
69106
EmbeddedTableTests.TopContainer.class
70107
})
71-
void testNestedUsage(DomainModelScope modelScope) {
108+
void testNestedModel(DomainModelScope modelScope) {
72109
final PersistentClass entityBinding = modelScope.getEntityBinding( TopContainer.class );
73110

74111
final Property subContainerProp = entityBinding.getProperty( "subContainer" );
@@ -77,6 +114,29 @@ void testNestedUsage(DomainModelScope modelScope) {
77114
final Property subContainersProp = entityBinding.getProperty( "subContainers" );
78115
final Collection containersPropValue = (Collection) subContainersProp.getValue();
79116
checkContainerComponent( (Component) containersPropValue.getElement(), "sub_containers" );
117+
118+
final Namespace dbNamespace = modelScope.getDomainModel().getDatabase().getDefaultNamespace();
119+
120+
// id, name
121+
final org.hibernate.mapping.Table primaryTable = dbNamespace.locateTable(
122+
Identifier.toIdentifier( "top" ) );
123+
assertThat( primaryTable.getColumns() ).hasSize( 2 );
124+
assertThat( primaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) )
125+
.containsExactlyInAnyOrder( "id", "name" );
126+
127+
// thing1, thing2, top_fk
128+
final org.hibernate.mapping.Table secondaryTable = dbNamespace.locateTable(
129+
Identifier.toIdentifier( "supp" ) );
130+
assertThat( secondaryTable.getColumns() ).hasSize( 3 );
131+
assertThat( secondaryTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) )
132+
.containsExactlyInAnyOrder( "thing1", "thing2", "top_fk" );
133+
134+
// thing1, thing2, top_fk
135+
final org.hibernate.mapping.Table collectionTable = dbNamespace.locateTable(
136+
Identifier.toIdentifier( "sub_containers" ) );
137+
assertThat( collectionTable.getColumns() ).hasSize( 3 );
138+
assertThat( collectionTable.getColumns().stream().map( org.hibernate.mapping.Column::getName ) )
139+
.containsExactlyInAnyOrder( "thing1", "thing2", "top_fk" );
80140
}
81141

82142
private void checkContainerComponent(Component containerComponent, String tableName) {
@@ -89,6 +149,49 @@ private void checkContainerComponent(Component containerComponent, String tableN
89149
} );
90150
}
91151

152+
@Test
153+
@ServiceRegistry
154+
@DomainModel(annotatedClasses = {EmbeddedTableTests.Tag.class, EmbeddedTableTests.Post.class})
155+
@SessionFactory(useCollectingStatementInspector = true)
156+
void testDatabase(SessionFactoryScope factoryScope) {
157+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
158+
sqlCollector.clear();
159+
160+
factoryScope.inTransaction( (session) -> {
161+
// NOTE: ... from posts p1_0 left join posts_secondary p1_1 ...
162+
session.createSelectionQuery( "from Post", Post.class ).list();
163+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
164+
assertThat( sqlCollector.getSqlQueries().get( 0 ) )
165+
.contains( "p1_0.id", "p1_0.name", "p1_1.added", "p1_1.text" );
166+
} );
167+
}
168+
169+
@Test
170+
@ServiceRegistry
171+
void testBadNestedPlacement(ServiceRegistryScope registryScope) {
172+
final HibernatePersistenceConfiguration persistenceConfiguration = registryScope
173+
.createPersistenceConfiguration( "bad-nested" )
174+
.managedClasses( Bottom.class, BadMiddle.class, BadNesterEntity.class );
175+
try ( var sf = persistenceConfiguration.createEntityManagerFactory() ) {
176+
fail( "Should have failed with AnnotationPlacementException" );
177+
}
178+
catch (AnnotationPlacementException expected) {
179+
}
180+
}
181+
182+
@Test
183+
@ServiceRegistry
184+
void testBadCollectionPlacement(ServiceRegistryScope registryScope) {
185+
final HibernatePersistenceConfiguration persistenceConfiguration = registryScope
186+
.createPersistenceConfiguration( "bad-nested" )
187+
.managedClasses( Bottom.class, Middle.class, BadCollectionEntity.class );
188+
try ( var sf = persistenceConfiguration.createEntityManagerFactory() ) {
189+
fail( "Should have failed with AnnotationPlacementException" );
190+
}
191+
catch (AnnotationPlacementException expected) {
192+
}
193+
}
194+
92195
@Embeddable
93196
public static class Tag {
94197
String text;
@@ -97,7 +200,7 @@ public static class Tag {
97200

98201
@Entity(name="Post")
99202
@Table(name="posts")
100-
@SecondaryTable(name="posts_secondary")
203+
@SecondaryTable(name="posts_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk"))
101204
public static class Post {
102205
@Id
103206
private Integer id;
@@ -109,7 +212,7 @@ public static class Post {
109212

110213
@Entity(name="PostCompliant")
111214
@Table(name="posts_compliant")
112-
@SecondaryTable(name="posts_compliant_secondary")
215+
@SecondaryTable(name="posts_compliant_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "post_fk"))
113216
public static class PostCompliant {
114217
@Id
115218
private Integer id;
@@ -129,13 +232,12 @@ public static class Nested {
129232
@Embeddable
130233
public static class Container {
131234
@Embedded
132-
@EmbeddedTable("should_be_ignored") // or maybe this should be an error? same for element-collections of embeddables
133235
Nested nested;
134236
}
135237

136238
@Entity(name="TopContainer")
137239
@Table(name="top")
138-
@SecondaryTable(name="supp")
240+
@SecondaryTable(name="supp", pkJoinColumns = @PrimaryKeyJoinColumn(name = "top_fk"))
139241
public static class TopContainer {
140242
@Id
141243
private Integer id;
@@ -145,8 +247,50 @@ public static class TopContainer {
145247
private Container subContainer;
146248

147249
@ElementCollection
148-
@CollectionTable(name = "sub_containers")
149-
@EmbeddedTable("supp")
250+
@CollectionTable(name = "sub_containers", joinColumns = @JoinColumn(name = "top_fk"))
150251
private Set<Container> subContainers;
151252
}
253+
254+
@Embeddable
255+
public static class Bottom {
256+
private String kind;
257+
private Instant whenReached;
258+
}
259+
260+
@Embeddable
261+
public static class BadMiddle {
262+
@Embedded
263+
@EmbeddedTable("secondary")
264+
private Bottom bottom;
265+
}
266+
267+
@Embeddable
268+
public static class Middle {
269+
@Embedded
270+
private Bottom bottom;
271+
}
272+
273+
@Entity(name="BadNesterEntity")
274+
@Table(name="primary")
275+
@SecondaryTable(name="secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "primary_fk"))
276+
public static class BadNesterEntity {
277+
@Id
278+
private Integer id;
279+
private String name;
280+
@Embedded
281+
@EmbeddedTable("secondary")
282+
BadMiddle badMiddle;
283+
}
284+
285+
@Entity(name="BadNesterEntity")
286+
@Table(name="primary")
287+
@SecondaryTable(name="secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "primary_fk"))
288+
public static class BadCollectionEntity {
289+
@Id
290+
private Integer id;
291+
private String name;
292+
@ElementCollection
293+
@EmbeddedTable("secondary")
294+
Set<Middle> middles;
295+
}
152296
}

hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/ServiceRegistryScope.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.util.function.Supplier;
1010

1111
import org.hibernate.boot.registry.StandardServiceRegistry;
12+
import org.hibernate.engine.config.spi.ConfigurationService;
13+
import org.hibernate.jpa.HibernatePersistenceConfiguration;
1214
import org.hibernate.service.Service;
1315

1416
/**
@@ -50,4 +52,15 @@ default <R, S extends Service> R fromService(Class<S> role, Function<S,R> action
5052

5153
return action.apply( service );
5254
}
55+
56+
default HibernatePersistenceConfiguration createPersistenceConfiguration(String persistenceUnitName) {
57+
final HibernatePersistenceConfiguration configuration = new HibernatePersistenceConfiguration( persistenceUnitName );
58+
final StandardServiceRegistry registry = getRegistry();
59+
60+
final ConfigurationService configurationService = registry.requireService( ConfigurationService.class );
61+
configuration.properties( configurationService.getSettings() );
62+
63+
return configuration;
64+
}
65+
5366
}

0 commit comments

Comments
 (0)