Skip to content

Commit 3f6ed37

Browse files
committed
HHH-19952: Add @CollectionTableOverride to override collection table names for embeddable collection fields
1 parent 48d74cf commit 3f6ed37

File tree

4 files changed

+599
-1
lines changed

4 files changed

+599
-1
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.annotations;
6+
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
import static java.lang.annotation.ElementType.FIELD;
11+
import static java.lang.annotation.ElementType.METHOD;
12+
import static java.lang.annotation.ElementType.TYPE;
13+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
14+
15+
/**
16+
* Used to override the collection table name for a collection
17+
* that is nested within an embeddable class.
18+
*
19+
* <p>This annotation allows overriding the collection table name
20+
* for collections that are defined within an embeddable class.
21+
*
22+
* <p>Example:
23+
* <pre>
24+
* &#064;Embeddable
25+
* public class Address {
26+
* &#064;ElementCollection
27+
* &#064;CollectionTable(name = "default_phones")
28+
* List&lt;Phone&gt; phones;
29+
* }
30+
*
31+
* &#064;Entity
32+
* public class Person {
33+
* &#064;Embedded
34+
* &#064;CollectionTableOverride(
35+
* name = "phones",
36+
* table = "person_phones"
37+
* )
38+
* Address address;
39+
* }
40+
* </pre>
41+
*/
42+
@Target({TYPE, METHOD, FIELD})
43+
@Retention(RUNTIME)
44+
public @interface CollectionTableOverride {
45+
/**
46+
* The path to the collection property within the embeddable.
47+
* For example, if the embeddable has a field "phones", the name would be "phones".
48+
* For nested embeddables, use dot notation like "address.phones".
49+
*/
50+
String name();
51+
52+
/**
53+
* The name of the collection table to use instead of the default.
54+
*/
55+
String table();
56+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.annotations;
6+
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
import static java.lang.annotation.ElementType.FIELD;
11+
import static java.lang.annotation.ElementType.METHOD;
12+
import static java.lang.annotation.ElementType.TYPE;
13+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
14+
15+
/**
16+
* Container for multiple {@link CollectionTableOverride} annotations.
17+
*/
18+
@Target({TYPE, METHOD, FIELD})
19+
@Retention(RUNTIME)
20+
public @interface CollectionTableOverrides {
21+
CollectionTableOverride[] value();
22+
}

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

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.hibernate.FetchMode;
2020
import org.hibernate.MappingException;
2121
import org.hibernate.annotations.*;
22+
import org.hibernate.annotations.CollectionTableOverride;
23+
import org.hibernate.annotations.CollectionTableOverrides;
2224
import org.hibernate.boot.model.IdentifierGeneratorDefinition;
2325
import org.hibernate.boot.models.AnnotationPlacementException;
2426
import org.hibernate.boot.models.JpaAnnotations;
@@ -103,6 +105,7 @@
103105
import static org.hibernate.boot.model.internal.BinderHelper.extractFromPackage;
104106
import static org.hibernate.boot.model.internal.BinderHelper.getFetchMode;
105107
import static org.hibernate.boot.model.internal.BinderHelper.getPath;
108+
import static org.hibernate.boot.model.internal.BinderHelper.getRelativePath;
106109
import static org.hibernate.boot.model.internal.BinderHelper.isDefault;
107110
import static org.hibernate.boot.model.internal.BinderHelper.isPrimitive;
108111
import static org.hibernate.boot.model.internal.DialectOverridesAnnotationHelper.getOverridableAnnotation;
@@ -620,6 +623,14 @@ private static void bindJoinedTableAssociation(
620623
final var assocTable = propertyHolder.getJoinTable( property );
621624
final var collectionTable = property.getDirectAnnotationUsage( CollectionTable.class );
622625

626+
// Check for @CollectionTableOverride annotation
627+
final var collectionTableOverride = findCollectionTableOverride(
628+
propertyHolder,
629+
property,
630+
inferredData,
631+
buildingContext
632+
);
633+
623634
final JoinColumn[] annJoins;
624635
final JoinColumn[] annInverseJoins;
625636
if ( assocTable != null || collectionTable != null ) {
@@ -636,7 +647,10 @@ private static void bindJoinedTableAssociation(
636647
if ( collectionTable != null ) {
637648
catalog = collectionTable.catalog();
638649
schema = collectionTable.schema();
639-
tableName = collectionTable.name();
650+
// Use overridden table name if @CollectionTableOverride is present
651+
tableName = collectionTableOverride != null
652+
? collectionTableOverride.table()
653+
: collectionTable.name();
640654
uniqueConstraints = collectionTable.uniqueConstraints();
641655
joins = collectionTable.joinColumns();
642656
inverseJoins = null;
@@ -2805,4 +2819,149 @@ else if ( isManyToAny ) {
28052819
}
28062820
}
28072821
}
2822+
2823+
/**
2824+
* Finds a @CollectionTableOverride annotation for the given collection property.
2825+
*
2826+
* @param propertyHolder The PropertyHolder containing the property
2827+
* @param property The collection property MemberDetails
2828+
* @param inferredData The PropertyData for the property
2829+
* @param buildingContext The MetadataBuildingContext
2830+
* @return The CollectionTableOverride if found, null otherwise
2831+
*/
2832+
private static CollectionTableOverride findCollectionTableOverride(
2833+
PropertyHolder propertyHolder,
2834+
MemberDetails property,
2835+
PropertyData inferredData,
2836+
MetadataBuildingContext buildingContext) {
2837+
2838+
// Get the entity's PersistentClass
2839+
final PersistentClass persistentClass = propertyHolder.getPersistentClass();
2840+
if (persistentClass == null) {
2841+
return null;
2842+
}
2843+
2844+
// Get the ClassDetails from the models context
2845+
final var modelsContext = buildingContext.getBootstrapContext().getModelsContext();
2846+
final ClassDetails entityClassDetails = modelsContext.getClassDetailsRegistry()
2847+
.resolveClassDetails(persistentClass.getClassName());
2848+
2849+
if (entityClassDetails == null) {
2850+
return null;
2851+
}
2852+
2853+
// Get the full relative path (e.g., "address.phones" for embeddable collections)
2854+
final String fullPath = getRelativePath(propertyHolder, property.getName());
2855+
2856+
// Extract the first part of the path (e.g., "address" from "address.phones")
2857+
// This is the field name in the entity class where @CollectionTableOverride is attached
2858+
final String fieldName;
2859+
if (fullPath.contains(".")) {
2860+
fieldName = fullPath.substring(0, fullPath.indexOf('.'));
2861+
} else {
2862+
// Direct collection on entity, use property name directly
2863+
fieldName = property.getName();
2864+
}
2865+
2866+
// Find the field property in the entity class
2867+
MemberDetails entityField = null;
2868+
try {
2869+
// Try to get the property from the entity class
2870+
for (MemberDetails member : entityClassDetails.getFields()) {
2871+
if (member.getName().equals(fieldName)) {
2872+
entityField = member;
2873+
break;
2874+
}
2875+
}
2876+
if (entityField == null) {
2877+
for (MemberDetails member : entityClassDetails.getMethods()) {
2878+
if (member.getName().equals(fieldName) ||
2879+
(member.getName().startsWith("get") &&
2880+
member.getName().substring(3).toLowerCase().equals(fieldName.toLowerCase()))) {
2881+
entityField = member;
2882+
break;
2883+
}
2884+
}
2885+
}
2886+
} catch (Exception e) {
2887+
// Ignore and fall back to class-level annotation
2888+
}
2889+
2890+
// Check field-level annotations first (if field found)
2891+
if (entityField != null) {
2892+
// Check for @CollectionTableOverrides (plural) on field
2893+
final var overridesAnnotation = entityField.getDirectAnnotationUsage(CollectionTableOverrides.class);
2894+
if (overridesAnnotation != null) {
2895+
for (CollectionTableOverride override : overridesAnnotation.value()) {
2896+
if (matchesPath(override.name(), propertyHolder, property, inferredData)) {
2897+
return override;
2898+
}
2899+
}
2900+
}
2901+
2902+
// Check for @CollectionTableOverride (singular) on field
2903+
final var singleOverride = entityField.getDirectAnnotationUsage(CollectionTableOverride.class);
2904+
if (singleOverride != null && matchesPath(
2905+
singleOverride.name(),
2906+
propertyHolder,
2907+
property,
2908+
inferredData)) {
2909+
return singleOverride;
2910+
}
2911+
}
2912+
2913+
// Fall back to class-level annotations
2914+
// Check for @CollectionTableOverrides (plural) on class
2915+
final var classOverridesAnnotation = entityClassDetails.getDirectAnnotationUsage(CollectionTableOverrides.class);
2916+
if (classOverridesAnnotation != null) {
2917+
for (CollectionTableOverride override : classOverridesAnnotation.value()) {
2918+
if (matchesPath(override.name(), propertyHolder, property, inferredData)) {
2919+
return override;
2920+
}
2921+
}
2922+
}
2923+
2924+
// Check for @CollectionTableOverride (singular) on class
2925+
final var classSingleOverride = entityClassDetails.getDirectAnnotationUsage(CollectionTableOverride.class);
2926+
if (classSingleOverride != null && matchesPath(
2927+
classSingleOverride.name(),
2928+
propertyHolder,
2929+
property,
2930+
inferredData)) {
2931+
return classSingleOverride;
2932+
}
2933+
2934+
return null;
2935+
}
2936+
2937+
/**
2938+
* Checks if the override name matches the current collection property path.
2939+
*
2940+
* @param overrideName The name from @CollectionTableOverride
2941+
* @param propertyHolder The PropertyHolder containing the property
2942+
* @param property The collection property MemberDetails
2943+
* @param inferredData The PropertyData for the property
2944+
* @return true if the path matches
2945+
*/
2946+
private static boolean matchesPath(
2947+
String overrideName,
2948+
PropertyHolder propertyHolder,
2949+
MemberDetails property,
2950+
PropertyData inferredData) {
2951+
2952+
// Get the full relative path (e.g., "address.phones" for embeddable collections)
2953+
final String fullPath = getRelativePath(propertyHolder, property.getName());
2954+
2955+
// Direct match: override name matches the full path
2956+
if (overrideName.equals(fullPath)) {
2957+
return true;
2958+
}
2959+
2960+
// Simple case: property name directly matches (for direct entity collections)
2961+
if (overrideName.equals(property.getName())) {
2962+
return true;
2963+
}
2964+
2965+
return false;
2966+
}
28082967
}

0 commit comments

Comments
 (0)