Skip to content

Commit 43ced16

Browse files
committed
HHH-19952: Support nested @CollectionTable in @CollectionTableOverride
Replace table name string with nested @CollectionTable annotation to allow full collection table configuration override, including schema, catalog, joinColumns, indexes, and other attributes.
1 parent e2170b7 commit 43ced16

File tree

3 files changed

+183
-70
lines changed

3 files changed

+183
-70
lines changed

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
import java.lang.annotation.Retention;
88
import java.lang.annotation.Target;
99

10+
import jakarta.persistence.CollectionTable;
11+
1012
import static java.lang.annotation.ElementType.FIELD;
1113
import static java.lang.annotation.ElementType.METHOD;
1214
import static java.lang.annotation.ElementType.TYPE;
1315
import static java.lang.annotation.RetentionPolicy.RUNTIME;
1416

1517
/**
16-
* Used to override the collection table name for a collection
18+
* Used to override the collection table configuration for a collection
1719
* that is nested within an embeddable class.
1820
*
19-
* <p>This annotation allows overriding the collection table name
21+
* <p>This annotation allows overriding the collection table configuration
2022
* for collections that are defined within an embeddable class.
2123
*
2224
* <p>Example:
@@ -33,7 +35,7 @@
3335
* &#064;Embedded
3436
* &#064;CollectionTableOverride(
3537
* name = "phones",
36-
* table = "person_phones"
38+
* collectionTable = &#064;CollectionTable(name = "person_phones")
3739
* )
3840
* Address address;
3941
* }
@@ -50,7 +52,9 @@
5052
String name();
5153

5254
/**
53-
* The name of the collection table to use instead of the default.
55+
* The collection table configuration to use instead of the default.
56+
* This allows specifying the full {@link CollectionTable} annotation
57+
* with all its attributes (name, schema, catalog, joinColumns, indexes, etc.).
5458
*/
55-
String table();
59+
CollectionTable collectionTable();
5660
}

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

Lines changed: 63 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ private static NotFoundAction notFoundAction(
357357
if ( notFound != null ) {
358358
if ( manyToManyAnn == null ) {
359359
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
360-
+ "' annotated '@NotFound' is not a '@ManyToMany' association" );
360+
+ "' annotated '@NotFound' is not a '@ManyToMany' association" );
361361
}
362362
return notFound.action();
363363
}
@@ -405,8 +405,8 @@ private static void checkAnnotations(
405405
if ( ( oneToMany != null || manyToMany != null || elementCollection != null )
406406
&& isToManyAssociationWithinEmbeddableCollection( propertyHolder ) ) {
407407
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData ) +
408-
"' belongs to an '@Embeddable' class that is contained in an '@ElementCollection' and may not be a "
409-
+ annotationName( oneToMany, manyToMany, elementCollection ) );
408+
"' belongs to an '@Embeddable' class that is contained in an '@ElementCollection' and may not be a "
409+
+ annotationName( oneToMany, manyToMany, elementCollection ) );
410410
}
411411

412412
if ( oneToMany != null && property.hasDirectAnnotationUsage( SoftDelete.class ) ) {
@@ -420,20 +420,20 @@ && isToManyAssociationWithinEmbeddableCollection( propertyHolder ) ) {
420420
&& manyToMany != null
421421
&& isNotBlank( manyToMany.mappedBy() ) ) {
422422
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData ) +
423-
"' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'" );
423+
"' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'" );
424424
}
425425

426426
if ( manyToMany != null || elementCollection != null ) {
427427
if ( property.hasDirectAnnotationUsage( JoinColumn.class )
428428
|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {
429429
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData )
430-
+ "' is a " + annotationName(
430+
+ "' is a " + annotationName(
431431
oneToMany,
432432
manyToMany,
433433
elementCollection
434434
)
435-
+ " and is directly annotated '@JoinColumn'"
436-
+ " (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')" );
435+
+ " and is directly annotated '@JoinColumn'"
436+
+ " (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')" );
437437
}
438438
}
439439
}
@@ -476,13 +476,13 @@ private static String handleTargetEntity(
476476
//TODO enhance exception with @ManyToAny and @CollectionOfElements
477477
if ( oneToManyAnn != null && manyToManyAnn != null ) {
478478
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData )
479-
+ "' is annotated both '@OneToMany' and '@ManyToMany'" );
479+
+ "' is annotated both '@OneToMany' and '@ManyToMany'" );
480480
}
481481
final String mappedBy;
482482
if ( oneToManyAnn != null ) {
483483
if ( joinColumns.isSecondary() ) {
484484
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
485-
+ "' has foreign key in secondary table" );
485+
+ "' has foreign key in secondary table" );
486486
}
487487
collectionBinder.setFkJoinColumns( joinColumns );
488488
mappedBy = nullIfEmpty( oneToManyAnn.mappedBy() );
@@ -497,7 +497,7 @@ private static String handleTargetEntity(
497497
else if ( elementCollectionAnn != null ) {
498498
if ( joinColumns.isSecondary() ) {
499499
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
500-
+ "' has foreign key in secondary table" );
500+
+ "' has foreign key in secondary table" );
501501
}
502502
collectionBinder.setFkJoinColumns( joinColumns );
503503
mappedBy = null;
@@ -657,9 +657,14 @@ private static void bindJoinedTableAssociation(
657657
buildingContext
658658
);
659659

660+
// If @CollectionTableOverride is present, use its collectionTable annotation
661+
final CollectionTable effectiveCollectionTable = collectionTableOverride != null
662+
? collectionTableOverride.collectionTable()
663+
: collectionTable;
664+
660665
final JoinColumn[] annJoins;
661666
final JoinColumn[] annInverseJoins;
662-
if ( assocTable != null || collectionTable != null ) {
667+
if ( assocTable != null || effectiveCollectionTable != null ) {
663668
final String catalog;
664669
final String schema;
665670
final String tableName;
@@ -670,18 +675,15 @@ private static void bindJoinedTableAssociation(
670675
final String options;
671676

672677
//JPA 2 has priority
673-
if ( collectionTable != null ) {
674-
catalog = collectionTable.catalog();
675-
schema = collectionTable.schema();
676-
// Use overridden table name if @CollectionTableOverride is present
677-
tableName = collectionTableOverride != null
678-
? collectionTableOverride.table()
679-
: collectionTable.name();
680-
uniqueConstraints = collectionTable.uniqueConstraints();
681-
joins = collectionTable.joinColumns();
678+
if ( effectiveCollectionTable != null ) {
679+
catalog = effectiveCollectionTable.catalog();
680+
schema = effectiveCollectionTable.schema();
681+
tableName = effectiveCollectionTable.name();
682+
uniqueConstraints = effectiveCollectionTable.uniqueConstraints();
683+
joins = effectiveCollectionTable.joinColumns();
682684
inverseJoins = null;
683-
jpaIndexes = collectionTable.indexes();
684-
options = collectionTable.options();
685+
jpaIndexes = effectiveCollectionTable.indexes();
686+
options = effectiveCollectionTable.options();
685687
}
686688
else {
687689
catalog = assocTable.catalog();
@@ -928,20 +930,20 @@ private static CollectionClassification determineCollectionClassification(
928930

929931
if ( property.hasAnnotationUsage( OrderColumn.class, modelsContext ) ) {
930932
throw new AnnotationException( "Attribute '"
931-
+ qualify(
933+
+ qualify(
932934
property.getDeclaringType().getName(),
933935
property.getName()
934936
)
935-
+ "' is annotated '@Bag' and may not also be annotated '@OrderColumn'" );
937+
+ "' is annotated '@Bag' and may not also be annotated '@OrderColumn'" );
936938
}
937939

938940
if ( property.hasAnnotationUsage( ListIndexBase.class, modelsContext ) ) {
939941
throw new AnnotationException( "Attribute '"
940-
+ qualify(
942+
+ qualify(
941943
property.getDeclaringType().getName(),
942944
property.getName()
943945
)
944-
+ "' is annotated '@Bag' and may not also be annotated '@ListIndexBase'" );
946+
+ "' is annotated '@Bag' and may not also be annotated '@ListIndexBase'" );
945947
}
946948

947949
final var collectionJavaType = property.getType().determineRawClass().toJavaClass();
@@ -1078,7 +1080,7 @@ private void setElementType(TypeDetails collectionElementType) {
10781080

10791081
private void setTargetEntity(Class<?> targetEntity) {
10801082
setTargetEntity( modelsContext().getClassDetailsRegistry()
1081-
.resolveClassDetails( targetEntity.getName() ) );
1083+
.resolveClassDetails( targetEntity.getName() ) );
10821084
}
10831085

10841086
private void setTargetEntity(ClassDetails targetEntity) {
@@ -1151,7 +1153,7 @@ private boolean isMutable() {
11511153
private void checkMapKeyColumn() {
11521154
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) && hasMapKeyProperty ) {
11531155
throw new AnnotationException( "Collection '" + qualify( propertyHolder.getPath(), propertyName )
1154-
+ "' is annotated both '@MapKey' and '@MapKeyColumn'" );
1156+
+ "' is annotated both '@MapKey' and '@MapKeyColumn'" );
11551157
}
11561158
}
11571159

@@ -1188,49 +1190,49 @@ private void detectMappedByProblem(boolean isMappedBy) {
11881190
if ( property.hasDirectAnnotationUsage( JoinColumn.class )
11891191
|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {
11901192
throw new AnnotationException( "Association '"
1191-
+ qualify( propertyHolder.getPath(), propertyName )
1192-
+ "' is 'mappedBy' another entity and may not specify the '@JoinColumn'" );
1193+
+ qualify( propertyHolder.getPath(), propertyName )
1194+
+ "' is 'mappedBy' another entity and may not specify the '@JoinColumn'" );
11931195
}
11941196
if ( propertyHolder.getJoinTable( property ) != null ) {
11951197
throw new AnnotationException( "Association '"
1196-
+ qualify( propertyHolder.getPath(), propertyName )
1197-
+ "' is 'mappedBy' another entity and may not specify the '@JoinTable'" );
1198+
+ qualify( propertyHolder.getPath(), propertyName )
1199+
+ "' is 'mappedBy' another entity and may not specify the '@JoinTable'" );
11981200
}
11991201
if ( oneToMany ) {
12001202
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) ) {
12011203
BOOT_LOGGER.warn( "Association '"
1202-
+ qualify( propertyHolder.getPath(), propertyName )
1203-
+ "' is 'mappedBy' another entity and should not specify a '@MapKeyColumn'"
1204-
+ " (use '@MapKey' instead)" );
1204+
+ qualify( propertyHolder.getPath(), propertyName )
1205+
+ "' is 'mappedBy' another entity and should not specify a '@MapKeyColumn'"
1206+
+ " (use '@MapKey' instead)" );
12051207
}
12061208
if ( property.hasDirectAnnotationUsage( OrderColumn.class ) ) {
12071209
BOOT_LOGGER.warn( "Association '"
1208-
+ qualify( propertyHolder.getPath(), propertyName )
1209-
+ "' is 'mappedBy' another entity and should not specify an '@OrderColumn'"
1210-
+ " (use '@OrderBy' instead)" );
1210+
+ qualify( propertyHolder.getPath(), propertyName )
1211+
+ "' is 'mappedBy' another entity and should not specify an '@OrderColumn'"
1212+
+ " (use '@OrderBy' instead)" );
12111213
}
12121214
}
12131215
else {
12141216
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) ) {
12151217
throw new AnnotationException( "Association '"
1216-
+ qualify( propertyHolder.getPath(), propertyName )
1217-
+ "' is 'mappedBy' another entity and may not specify a '@MapKeyColumn'"
1218-
+ " (use '@MapKey' instead)" );
1218+
+ qualify( propertyHolder.getPath(), propertyName )
1219+
+ "' is 'mappedBy' another entity and may not specify a '@MapKeyColumn'"
1220+
+ " (use '@MapKey' instead)" );
12191221
}
12201222
if ( property.hasDirectAnnotationUsage( OrderColumn.class ) ) {
12211223
throw new AnnotationException( "Association '"
1222-
+ qualify( propertyHolder.getPath(), propertyName )
1223-
+ "' is 'mappedBy' another entity and may not specify an '@OrderColumn'"
1224-
+ " (use '@OrderBy' instead)" );
1224+
+ qualify( propertyHolder.getPath(), propertyName )
1225+
+ "' is 'mappedBy' another entity and may not specify an '@OrderColumn'"
1226+
+ " (use '@OrderBy' instead)" );
12251227
}
12261228
}
12271229
}
12281230
else if ( oneToMany
12291231
&& property.hasDirectAnnotationUsage( OnDelete.class )
12301232
&& !hasExplicitJoinColumn() ) {
12311233
throw new AnnotationException( "Unidirectional '@OneToMany' association '"
1232-
+ qualify( propertyHolder.getPath(), propertyName )
1233-
+ "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'" );
1234+
+ qualify( propertyHolder.getPath(), propertyName )
1235+
+ "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'" );
12341236
}
12351237
}
12361238

@@ -1524,7 +1526,7 @@ TypeDetails getElementType() {
15241526
}
15251527
else {
15261528
throw new AnnotationException( "Collection '" + safeCollectionRole()
1527-
+ "' is declared with a raw type and has an explicit 'targetEntity'" );
1529+
+ "' is declared with a raw type and has an explicit 'targetEntity'" );
15281530
}
15291531
}
15301532
else {
@@ -1842,7 +1844,7 @@ private void addFilterJoinTable(boolean hasAssociationTable, FilterJoinTable fil
18421844
}
18431845
else {
18441846
throw new AnnotationException( "Collection '" + qualify( propertyHolder.getPath(), propertyName )
1845-
+ "' is an association with no join table and may not have a '@FilterJoinTable'" );
1847+
+ "' is an association with no join table and may not have a '@FilterJoinTable'" );
18461848
}
18471849
}
18481850

@@ -1864,15 +1866,15 @@ private String getDefaultFilterCondition(String name, Annotation annotation) {
18641866
final var definition = getMetadataCollector().getFilterDefinition( name );
18651867
if ( definition == null ) {
18661868
throw new AnnotationException( "Collection '" + qualify( propertyHolder.getPath(), propertyName )
1867-
+ "' has a '@" + annotation.annotationType().getSimpleName()
1868-
+ "' for an undefined filter named '" + name + "'" );
1869+
+ "' has a '@" + annotation.annotationType().getSimpleName()
1870+
+ "' for an undefined filter named '" + name + "'" );
18691871
}
18701872
final String defaultCondition = definition.getDefaultFilterCondition();
18711873
if ( isBlank( defaultCondition ) ) {
18721874
throw new AnnotationException( "Collection '" + qualify( propertyHolder.getPath(), propertyName ) +
1873-
"' has a '@" + annotation.annotationType().getSimpleName()
1874-
+ "' with no 'condition' and no default condition was given by the '@FilterDef' named '"
1875-
+ name + "'" );
1875+
"' has a '@" + annotation.annotationType().getSimpleName()
1876+
+ "' with no 'condition' and no default condition was given by the '@FilterDef' named '"
1877+
+ name + "'" );
18761878
}
18771879
return defaultCondition;
18781880
}
@@ -2483,8 +2485,8 @@ private static void addCheckToCollection(Table collectionTable, Check check) {
24832485
final String name = check.name();
24842486
final String constraint = check.constraints();
24852487
collectionTable.addCheck( name.isBlank()
2486-
? new CheckConstraint( constraint )
2487-
: new CheckConstraint( name, constraint ) );
2488+
? new CheckConstraint( constraint )
2489+
: new CheckConstraint( name, constraint ) );
24882490
}
24892491

24902492
private void processSoftDeletes() {
@@ -2513,7 +2515,7 @@ private void handleUnownedManyToMany(
25132515
boolean isCollectionOfEntities) {
25142516
if ( !isCollectionOfEntities ) {
25152517
throw new AnnotationException( "Association '" + safeCollectionRole() + "'"
2516-
+ targetEntityMessage( elementType ) );
2518+
+ targetEntityMessage( elementType ) );
25172519
}
25182520

25192521
joinColumns.setManyToManyOwnerSideEntityName( collectionEntity.getEntityName() );
@@ -2536,7 +2538,7 @@ private void checkCheckAnnotation() {
25362538
if ( property.hasDirectAnnotationUsage( Checks.class )
25372539
|| property.hasDirectAnnotationUsage( Check.class ) ) {
25382540
throw new AnnotationException( "Association '" + safeCollectionRole()
2539-
+ " is an unowned collection and may not be annotated '@Check'" );
2541+
+ " is an unowned collection and may not be annotated '@Check'" );
25402542
}
25412543
}
25422544

@@ -2549,20 +2551,20 @@ private void detectManyToManyProblems(
25492551
if ( property.hasDirectAnnotationUsage( ManyToMany.class )
25502552
|| property.hasDirectAnnotationUsage( OneToMany.class ) ) {
25512553
throw new AnnotationException( "Association '" + safeCollectionRole() + "'"
2552-
+ targetEntityMessage( elementType ) );
2554+
+ targetEntityMessage( elementType ) );
25532555
}
25542556
else if ( isManyToAny ) {
25552557
if ( propertyHolder.getJoinTable( property ) == null ) {
25562558
throw new AnnotationException( "Association '" + safeCollectionRole()
2557-
+ "' is a '@ManyToAny' and must specify a '@JoinTable'" );
2559+
+ "' is a '@ManyToAny' and must specify a '@JoinTable'" );
25582560
}
25592561
}
25602562
else {
25612563
final var joinTableAnn = propertyHolder.getJoinTable( property );
25622564
if ( joinTableAnn != null && !ArrayHelper.isEmpty( joinTableAnn.inverseJoinColumns() ) ) {
25632565
throw new AnnotationException( "Association '" + safeCollectionRole()
2564-
+ " has a '@JoinTable' with 'inverseJoinColumns' and"
2565-
+ targetEntityMessage( elementType ) );
2566+
+ " has a '@JoinTable' with 'inverseJoinColumns' and"
2567+
+ targetEntityMessage( elementType ) );
25662568
}
25672569
}
25682570
}

0 commit comments

Comments
 (0)