diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverride.java b/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverride.java new file mode 100644 index 000000000000..e6f75297c430 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverride.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.persistence.CollectionTable; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Used to override the collection table configuration for a collection + * that is nested within an embeddable class. + * + *
This annotation allows overriding the collection table configuration + * for collections that are defined within an embeddable class. + * + *
Example: + *
+ * @Embeddable
+ * public class Address {
+ * @ElementCollection
+ * @CollectionTable(name = "default_phones")
+ * List<Phone> phones;
+ * }
+ *
+ * @Entity
+ * public class Person {
+ * @Embedded
+ * @CollectionTableOverride(
+ * name = "phones",
+ * collectionTable = @CollectionTable(name = "person_phones")
+ * )
+ * Address address;
+ * }
+ *
+ */
+@Target({ TYPE, METHOD, FIELD })
+@Retention(RUNTIME)
+public @interface CollectionTableOverride {
+ /**
+ * The path to the collection property within the embeddable.
+ * For example, if the embeddable has a field "phones", the name would be "phones".
+ * For nested embeddables, use dot notation like "address.phones".
+ */
+ String name();
+
+ /**
+ * The collection table configuration to use instead of the default.
+ * This allows specifying the full {@link CollectionTable} annotation
+ * with all its attributes (name, schema, catalog, joinColumns, indexes, etc.).
+ */
+ CollectionTable collectionTable();
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverrides.java b/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverrides.java
new file mode 100644
index 000000000000..bb2b6a103adf
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/annotations/CollectionTableOverrides.java
@@ -0,0 +1,22 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Container for multiple {@link CollectionTableOverride} annotations.
+ */
+@Target({ TYPE, METHOD, FIELD })
+@Retention(RUNTIME)
+public @interface CollectionTableOverrides {
+ CollectionTableOverride[] value();
+}
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 f60ab5e44969..50acd1a26648 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
@@ -19,6 +19,8 @@
import org.hibernate.FetchMode;
import org.hibernate.MappingException;
import org.hibernate.annotations.*;
+import org.hibernate.annotations.CollectionTableOverride;
+import org.hibernate.annotations.CollectionTableOverrides;
import org.hibernate.boot.model.IdentifierGeneratorDefinition;
import org.hibernate.boot.models.AnnotationPlacementException;
import org.hibernate.boot.models.JpaAnnotations;
@@ -103,6 +105,7 @@
import static org.hibernate.boot.model.internal.BinderHelper.extractFromPackage;
import static org.hibernate.boot.model.internal.BinderHelper.getFetchMode;
import static org.hibernate.boot.model.internal.BinderHelper.getPath;
+import static org.hibernate.boot.model.internal.BinderHelper.getRelativePath;
import static org.hibernate.boot.model.internal.BinderHelper.isDefault;
import static org.hibernate.boot.model.internal.BinderHelper.isPrimitive;
import static org.hibernate.boot.model.internal.DialectOverridesAnnotationHelper.getOverridableAnnotation;
@@ -220,9 +223,20 @@ public static void bindCollection(
final var oneToManyAnn = memberDetails.getAnnotationUsage( OneToMany.class, modelsContext );
final var manyToManyAnn = memberDetails.getAnnotationUsage( ManyToMany.class, modelsContext );
final var elementCollectionAnn = memberDetails.getAnnotationUsage( ElementCollection.class, modelsContext );
- checkAnnotations( propertyHolder, inferredData, memberDetails, oneToManyAnn, manyToManyAnn, elementCollectionAnn );
+ checkAnnotations(
+ propertyHolder,
+ inferredData,
+ memberDetails,
+ oneToManyAnn,
+ manyToManyAnn,
+ elementCollectionAnn
+ );
- final var collectionBinder = getCollectionBinder( memberDetails, hasMapKeyAnnotation( memberDetails ), context );
+ final var collectionBinder = getCollectionBinder(
+ memberDetails,
+ hasMapKeyAnnotation( memberDetails ),
+ context
+ );
collectionBinder.setIndexColumn( getIndexColumn( propertyHolder, inferredData, entityBinder, context ) );
collectionBinder.setMapKey( memberDetails.getAnnotationUsage( MapKey.class, modelsContext ) );
collectionBinder.setPropertyName( inferredData.getPropertyName() );
@@ -231,10 +245,19 @@ public static void bindCollection(
collectionBinder.setNaturalSort( memberDetails.getAnnotationUsage( SortNatural.class, modelsContext ) );
collectionBinder.setComparatorSort( memberDetails.getAnnotationUsage( SortComparator.class, modelsContext ) );
collectionBinder.setCache( memberDetails.getAnnotationUsage( Cache.class, modelsContext ) );
- collectionBinder.setQueryCacheLayout( memberDetails.getAnnotationUsage( QueryCacheLayout.class, modelsContext ) );
- collectionBinder.setPropertyHolder(propertyHolder);
+ collectionBinder.setQueryCacheLayout( memberDetails.getAnnotationUsage(
+ QueryCacheLayout.class,
+ modelsContext
+ ) );
+ collectionBinder.setPropertyHolder( propertyHolder );
- collectionBinder.setNotFoundAction( notFoundAction( propertyHolder, inferredData, memberDetails, manyToManyAnn, modelsContext ) );
+ collectionBinder.setNotFoundAction( notFoundAction(
+ propertyHolder,
+ inferredData,
+ memberDetails,
+ manyToManyAnn,
+ modelsContext
+ ) );
collectionBinder.setElementType( inferredData.getClassOrElementType() );
collectionBinder.setAccessType( inferredData.getDefaultAccess() );
collectionBinder.setEmbedded( memberDetails.hasAnnotationUsage( Embedded.class, modelsContext ) );
@@ -333,8 +356,8 @@ private static NotFoundAction notFoundAction(
final var notFound = property.getAnnotationUsage( NotFound.class, sourceModelContext );
if ( notFound != null ) {
if ( manyToManyAnn == null ) {
- throw new AnnotationException( "Collection '" + getPath(propertyHolder, inferredData)
- + "' annotated '@NotFound' is not a '@ManyToMany' association" );
+ throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
+ + "' annotated '@NotFound' is not a '@ManyToMany' association" );
}
return notFound.action();
}
@@ -369,7 +392,7 @@ private static PropertyData virtualPropertyData(PropertyData inferredData, Membe
//do not use "element" if you are a JPA 2 @ElementCollection, only for legacy Hibernate mappings
return property.hasDirectAnnotationUsage( ElementCollection.class )
? inferredData
- : new WrappedInferredData(inferredData, "element" );
+ : new WrappedInferredData( inferredData, "element" );
}
private static void checkAnnotations(
@@ -382,8 +405,8 @@ private static void checkAnnotations(
if ( ( oneToMany != null || manyToMany != null || elementCollection != null )
&& isToManyAssociationWithinEmbeddableCollection( propertyHolder ) ) {
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData ) +
- "' belongs to an '@Embeddable' class that is contained in an '@ElementCollection' and may not be a "
- + annotationName( oneToMany, manyToMany, elementCollection ));
+ "' belongs to an '@Embeddable' class that is contained in an '@ElementCollection' and may not be a "
+ + annotationName( oneToMany, manyToMany, elementCollection ) );
}
if ( oneToMany != null && property.hasDirectAnnotationUsage( SoftDelete.class ) ) {
@@ -396,17 +419,21 @@ && isToManyAssociationWithinEmbeddableCollection( propertyHolder ) ) {
if ( property.hasDirectAnnotationUsage( OrderColumn.class )
&& manyToMany != null
&& isNotBlank( manyToMany.mappedBy() ) ) {
- throw new AnnotationException("Collection '" + getPath( propertyHolder, inferredData ) +
- "' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'");
+ throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData ) +
+ "' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'" );
}
if ( manyToMany != null || elementCollection != null ) {
if ( property.hasDirectAnnotationUsage( JoinColumn.class )
|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData )
- + "' is a " + annotationName( oneToMany, manyToMany, elementCollection )
- + " and is directly annotated '@JoinColumn'"
- + " (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')" );
+ + "' is a " + annotationName(
+ oneToMany,
+ manyToMany,
+ elementCollection
+ )
+ + " and is directly annotated '@JoinColumn'"
+ + " (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')" );
}
}
}
@@ -449,26 +476,28 @@ private static String handleTargetEntity(
//TODO enhance exception with @ManyToAny and @CollectionOfElements
if ( oneToManyAnn != null && manyToManyAnn != null ) {
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData )
- + "' is annotated both '@OneToMany' and '@ManyToMany'" );
+ + "' is annotated both '@OneToMany' and '@ManyToMany'" );
}
final String mappedBy;
if ( oneToManyAnn != null ) {
if ( joinColumns.isSecondary() ) {
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
- + "' has foreign key in secondary table" );
+ + "' has foreign key in secondary table" );
}
collectionBinder.setFkJoinColumns( joinColumns );
mappedBy = nullIfEmpty( oneToManyAnn.mappedBy() );
collectionBinder.setTargetEntity( oneToManyAnn.targetEntity() );
collectionBinder.setCascadeStrategy(
- aggregateCascadeTypes( oneToManyAnn.cascade(), hibernateCascade,
- oneToManyAnn.orphanRemoval(), context ) );
+ aggregateCascadeTypes(
+ oneToManyAnn.cascade(), hibernateCascade,
+ oneToManyAnn.orphanRemoval(), context
+ ) );
collectionBinder.setOneToMany( true );
}
else if ( elementCollectionAnn != null ) {
if ( joinColumns.isSecondary() ) {
throw new AnnotationException( "Collection '" + getPath( propertyHolder, inferredData )
- + "' has foreign key in secondary table" );
+ + "' has foreign key in secondary table" );
}
collectionBinder.setFkJoinColumns( joinColumns );
mappedBy = null;
@@ -497,17 +526,17 @@ else if ( property.hasDirectAnnotationUsage( ManyToAny.class ) ) {
}
private static boolean hasMapKeyAnnotation(MemberDetails property) {
- return property.hasDirectAnnotationUsage(MapKeyJavaType.class)
- || property.hasDirectAnnotationUsage(MapKeyJdbcType.class)
- || property.hasDirectAnnotationUsage(MapKeyJdbcTypeCode.class)
- || property.hasDirectAnnotationUsage(MapKeyMutability.class)
- || property.hasDirectAnnotationUsage(MapKey.class)
- || property.hasDirectAnnotationUsage(MapKeyType.class);
+ return property.hasDirectAnnotationUsage( MapKeyJavaType.class )
+ || property.hasDirectAnnotationUsage( MapKeyJdbcType.class )
+ || property.hasDirectAnnotationUsage( MapKeyJdbcTypeCode.class )
+ || property.hasDirectAnnotationUsage( MapKeyMutability.class )
+ || property.hasDirectAnnotationUsage( MapKey.class )
+ || property.hasDirectAnnotationUsage( MapKeyType.class );
}
private static boolean isToManyAssociationWithinEmbeddableCollection(PropertyHolder propertyHolder) {
return propertyHolder instanceof ComponentPropertyHolder componentPropertyHolder
- && componentPropertyHolder.isWithinElementCollection();
+ && componentPropertyHolder.isWithinElementCollection();
}
private static AnnotatedColumns elementColumns(
@@ -532,7 +561,7 @@ private static AnnotatedColumns elementColumns(
}
else if ( property.hasDirectAnnotationUsage( Formula.class ) ) {
return buildFormulaFromAnnotation(
- getOverridableAnnotation(property, Formula.class, context),
+ getOverridableAnnotation( property, Formula.class, context ),
// comment,
nullability,
propertyHolder,
@@ -594,9 +623,9 @@ private static AnnotatedColumns mapKeyColumns(
return buildColumnFromAnnotations(
property.hasDirectAnnotationUsage( MapKeyColumn.class )
? MapKeyColumnJpaAnnotation.toColumnAnnotation(
- property.getDirectAnnotationUsage( MapKeyColumn.class ),
- context.getBootstrapContext().getModelsContext()
- )
+ property.getDirectAnnotationUsage( MapKeyColumn.class ),
+ context.getBootstrapContext().getModelsContext()
+ )
: null,
// comment,
Nullability.FORCED_NOT_NULL,
@@ -620,9 +649,22 @@ private static void bindJoinedTableAssociation(
final var assocTable = propertyHolder.getJoinTable( property );
final var collectionTable = property.getDirectAnnotationUsage( CollectionTable.class );
+ // Check for @CollectionTableOverride annotation
+ final var collectionTableOverride = findCollectionTableOverride(
+ propertyHolder,
+ property,
+ inferredData,
+ buildingContext
+ );
+
+ // If @CollectionTableOverride is present, use its collectionTable annotation
+ final CollectionTable effectiveCollectionTable = collectionTableOverride != null
+ ? collectionTableOverride.collectionTable()
+ : collectionTable;
+
final JoinColumn[] annJoins;
final JoinColumn[] annInverseJoins;
- if ( assocTable != null || collectionTable != null ) {
+ if ( assocTable != null || effectiveCollectionTable != null ) {
final String catalog;
final String schema;
final String tableName;
@@ -633,15 +675,15 @@ private static void bindJoinedTableAssociation(
final String options;
//JPA 2 has priority
- if ( collectionTable != null ) {
- catalog = collectionTable.catalog();
- schema = collectionTable.schema();
- tableName = collectionTable.name();
- uniqueConstraints = collectionTable.uniqueConstraints();
- joins = collectionTable.joinColumns();
+ if ( effectiveCollectionTable != null ) {
+ catalog = effectiveCollectionTable.catalog();
+ schema = effectiveCollectionTable.schema();
+ tableName = effectiveCollectionTable.name();
+ uniqueConstraints = effectiveCollectionTable.uniqueConstraints();
+ joins = effectiveCollectionTable.joinColumns();
inverseJoins = null;
- jpaIndexes = collectionTable.indexes();
- options = collectionTable.options();
+ jpaIndexes = effectiveCollectionTable.indexes();
+ options = effectiveCollectionTable.options();
}
else {
catalog = assocTable.catalog();
@@ -771,8 +813,10 @@ private static CollectionBinder getCollectionBinder(
boolean isHibernateExtensionMapping,
MetadataBuildingContext buildingContext) {
final var typeAnnotation =
- property.getAnnotationUsage( CollectionType.class,
- buildingContext.getBootstrapContext().getModelsContext() );
+ property.getAnnotationUsage(
+ CollectionType.class,
+ buildingContext.getBootstrapContext().getModelsContext()
+ );
final var binder = typeAnnotation != null
? createBinderFromCustomTypeAnnotation( property, typeAnnotation, buildingContext )
: createBinderAutomatically( property, buildingContext );
@@ -877,19 +921,29 @@ private static CollectionClassification determineCollectionClassification(
final var modelsContext = buildingContext.getBootstrapContext().getModelsContext();
if ( !property.hasAnnotationUsage( Bag.class, modelsContext ) ) {
- return determineCollectionClassification( determineSemanticJavaType( property ), property, buildingContext );
+ return determineCollectionClassification(
+ determineSemanticJavaType( property ),
+ property,
+ buildingContext
+ );
}
if ( property.hasAnnotationUsage( OrderColumn.class, modelsContext ) ) {
throw new AnnotationException( "Attribute '"
- + qualify( property.getDeclaringType().getName(), property.getName() )
- + "' is annotated '@Bag' and may not also be annotated '@OrderColumn'" );
+ + qualify(
+ property.getDeclaringType().getName(),
+ property.getName()
+ )
+ + "' is annotated '@Bag' and may not also be annotated '@OrderColumn'" );
}
if ( property.hasAnnotationUsage( ListIndexBase.class, modelsContext ) ) {
throw new AnnotationException( "Attribute '"
- + qualify( property.getDeclaringType().getName(), property.getName() )
- + "' is annotated '@Bag' and may not also be annotated '@ListIndexBase'" );
+ + qualify(
+ property.getDeclaringType().getName(),
+ property.getName()
+ )
+ + "' is annotated '@Bag' and may not also be annotated '@ListIndexBase'" );
}
final var collectionJavaType = property.getType().determineRawClass().toJavaClass();
@@ -1026,7 +1080,7 @@ private void setElementType(TypeDetails collectionElementType) {
private void setTargetEntity(Class> targetEntity) {
setTargetEntity( modelsContext().getClassDetailsRegistry()
- .resolveClassDetails( targetEntity.getName() ) );
+ .resolveClassDetails( targetEntity.getName() ) );
}
private void setTargetEntity(ClassDetails targetEntity) {
@@ -1057,7 +1111,8 @@ private void bind() {
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" );
+ throw new AnnotationPlacementException(
+ "@EmbeddedTable only supported for use on entity or mapped-superclass" );
}
}
collection = createCollection( propertyHolder.getPersistentClass() );
@@ -1098,7 +1153,7 @@ private boolean isMutable() {
private void checkMapKeyColumn() {
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) && hasMapKeyProperty ) {
throw new AnnotationException( "Collection '" + qualify( propertyHolder.getPath(), propertyName )
- + "' is annotated both '@MapKey' and '@MapKeyColumn'" );
+ + "' is annotated both '@MapKey' and '@MapKeyColumn'" );
}
}
@@ -1109,7 +1164,7 @@ private void scheduleSecondPass(boolean isMappedBy) {
metadataCollector.addMappedBy( getElementType().getName(), mappedBy, propertyName );
}
- if ( inheritanceStatePerClass == null) {
+ if ( inheritanceStatePerClass == null ) {
throw new AssertionFailure( "inheritanceStatePerClass not set" );
}
metadataCollector.addSecondPass( getSecondPass(), !isMappedBy );
@@ -1135,40 +1190,40 @@ private void detectMappedByProblem(boolean isMappedBy) {
if ( property.hasDirectAnnotationUsage( JoinColumn.class )
|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {
throw new AnnotationException( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and may not specify the '@JoinColumn'" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and may not specify the '@JoinColumn'" );
}
if ( propertyHolder.getJoinTable( property ) != null ) {
throw new AnnotationException( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and may not specify the '@JoinTable'" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and may not specify the '@JoinTable'" );
}
if ( oneToMany ) {
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) ) {
BOOT_LOGGER.warn( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and should not specify a '@MapKeyColumn'"
- + " (use '@MapKey' instead)" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and should not specify a '@MapKeyColumn'"
+ + " (use '@MapKey' instead)" );
}
if ( property.hasDirectAnnotationUsage( OrderColumn.class ) ) {
BOOT_LOGGER.warn( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and should not specify an '@OrderColumn'"
- + " (use '@OrderBy' instead)" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and should not specify an '@OrderColumn'"
+ + " (use '@OrderBy' instead)" );
}
}
else {
if ( property.hasDirectAnnotationUsage( MapKeyColumn.class ) ) {
throw new AnnotationException( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and may not specify a '@MapKeyColumn'"
- + " (use '@MapKey' instead)" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and may not specify a '@MapKeyColumn'"
+ + " (use '@MapKey' instead)" );
}
if ( property.hasDirectAnnotationUsage( OrderColumn.class ) ) {
throw new AnnotationException( "Association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is 'mappedBy' another entity and may not specify an '@OrderColumn'"
- + " (use '@OrderBy' instead)" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is 'mappedBy' another entity and may not specify an '@OrderColumn'"
+ + " (use '@OrderBy' instead)" );
}
}
}
@@ -1176,17 +1231,17 @@ else if ( oneToMany
&& property.hasDirectAnnotationUsage( OnDelete.class )
&& !hasExplicitJoinColumn() ) {
throw new AnnotationException( "Unidirectional '@OneToMany' association '"
- + qualify( propertyHolder.getPath(), propertyName )
- + "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'" );
+ + qualify( propertyHolder.getPath(), propertyName )
+ + "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'" );
}
}
private boolean hasExplicitJoinColumn() {
return property.hasDirectAnnotationUsage( JoinColumn.class )
- || property.hasDirectAnnotationUsage( JoinColumns.class )
- || property.hasDirectAnnotationUsage( JoinTable.class )
- && property.getDirectAnnotationUsage( JoinTable.class )
- .joinColumns().length > 0;
+ || property.hasDirectAnnotationUsage( JoinColumns.class )
+ || property.hasDirectAnnotationUsage( JoinTable.class )
+ && property.getDirectAnnotationUsage( JoinTable.class )
+ .joinColumns().length > 0;
}
private void bindProperty() {
@@ -1309,7 +1364,7 @@ else if ( comparatorSort != null ) {
if ( jpaOrderBy != null && sqlOrder != null ) {
throw buildIllegalOrderCombination();
}
- final boolean ordered = jpaOrderBy != null || sqlOrder != null ;
+ final boolean ordered = jpaOrderBy != null || sqlOrder != null;
if ( ordered ) {
// we can only apply the sql-based order by up front. The jpa order by has to wait for second pass
if ( sqlOrder != null ) {
@@ -1391,14 +1446,16 @@ private ModelsContext modelsContext() {
}
private void handleFetchProfileOverrides() {
- property.forEachAnnotationUsage( FetchProfileOverride.class, modelsContext(), (usage) -> {
- getMetadataCollector().addSecondPass( new FetchSecondPass(
- usage,
- propertyHolder,
- propertyName,
- buildingContext
- ) );
- } );
+ property.forEachAnnotationUsage(
+ FetchProfileOverride.class, modelsContext(), (usage) -> {
+ getMetadataCollector().addSecondPass( new FetchSecondPass(
+ usage,
+ propertyHolder,
+ propertyName,
+ buildingContext
+ ) );
+ }
+ );
}
private void handleFetch() {
@@ -1414,7 +1471,7 @@ private void handleFetch() {
private void setHibernateFetchMode(org.hibernate.annotations.FetchMode fetchMode) {
switch ( fetchMode ) {
- case JOIN :
+ case JOIN:
collection.setFetchMode( FetchMode.JOIN );
collection.setLazy( false );
break;
@@ -1469,7 +1526,7 @@ TypeDetails getElementType() {
}
else {
throw new AnnotationException( "Collection '" + safeCollectionRole()
- + "' is declared with a raw type and has an explicit 'targetEntity'" );
+ + "' is declared with a raw type and has an explicit 'targetEntity'" );
}
}
else {
@@ -1523,8 +1580,8 @@ private Property getMappedByProperty(TypeDetails elementType, PersistentClass pe
catch (MappingException e) {
throw new AnnotationException(
"Collection '" + safeCollectionRole()
- + "' is 'mappedBy' a property named '" + mappedBy
- + "' which does not exist in the target entity '" + elementType.getName() + "'"
+ + "' is 'mappedBy' a property named '" + mappedBy
+ + "' which does not exist in the target entity '" + elementType.getName() + "'"
);
}
}
@@ -1532,19 +1589,19 @@ private Property getMappedByProperty(TypeDetails elementType, PersistentClass pe
private boolean noAssociationTable(Map+ * This test verifies that the @CollectionTableOverride annotation correctly + * overrides collection table names for collections within embeddable classes. + * + * @author Test + */ +@DomainModel( + annotatedClasses = { + CollectionTableOverrideTest.Person.class, + CollectionTableOverrideTest.Company.class, + CollectionTableOverrideTest.Organization.class, + CollectionTableOverrideTest.Customer.class + } +) +@SessionFactory +public class CollectionTableOverrideTest { + + /** + * Test purpose: Verify that @CollectionTableOverride is correctly applied at the metadata level. + *
+ * Verification: + * 1. Verify that Person entity's address.phones collection is overridden to table name "person_phones" + * 2. Verify that Company entity's address.phones collection is overridden to table name "company_phones" + * 3. Verify that Organization entity's contactInfo.emails collection is overridden to table name "organization_emails" + */ + @Test + public void testCollectionTableNameOverride(DomainModelScope scope) { + final MetadataImplementor metadata = scope.getDomainModel(); + + // Verify Person entity's address.phones collection table name + final Collection personPhonesCollection = metadata.getCollectionBinding( + Person.class.getName() + ".address.phones" + ); + assertThat( + "Collection table name should be overridden to 'person_phones'", + personPhonesCollection.getCollectionTable().getName(), + is( "person_phones" ) + ); + + // Verify Company entity's address.phones collection table name + final Collection companyPhonesCollection = metadata.getCollectionBinding( + Company.class.getName() + ".address.phones" + ); + assertThat( + "Collection table name should be overridden to 'company_phones'", + companyPhonesCollection.getCollectionTable().getName(), + is( "company_phones" ) + ); + + // Verify Organization entity's contactInfo.emails collection table name (using @CollectionTableOverrides) + final Collection orgEmailsCollection = metadata.getCollectionBinding( + Organization.class.getName() + ".contactInfo.emails" + ); + assertThat( + "Collection table name should be overridden to 'organization_emails'", + orgEmailsCollection.getCollectionTable().getName(), + is( "organization_emails" ) + ); + } + + /** + * Test purpose: Verify that overridden table names are correctly reflected in the schema during DDL generation. + *
+ * Verification: + * 1. Verify that overridden table names (person_phones, company_phones, organization_emails) exist in the schema + * 2. Verify that default table names (default_phones, default_emails) do NOT exist in the schema + */ + @Test + public void testSchemaGeneration(DomainModelScope scope) { + final MetadataImplementor metadata = scope.getDomainModel(); + + // Verify that overridden table names exist in the schema + assertTrue( + SchemaUtil.isTablePresent( "person_phones", metadata ), + "Table 'person_phones' should be present in schema" + ); + assertTrue( + SchemaUtil.isTablePresent( "company_phones", metadata ), + "Table 'company_phones' should be present in schema" + ); + assertTrue( + SchemaUtil.isTablePresent( "organization_emails", metadata ), + "Table 'organization_emails' should be present in schema" + ); + + // Verify that default table names do NOT exist in the schema + assertFalse( + SchemaUtil.isTablePresent( "default_phones", metadata ), + "Default table 'default_phones' should NOT be present in schema" + ); + assertFalse( + SchemaUtil.isTablePresent( "default_emails", metadata ), + "Default table 'default_emails' should NOT be present in schema" + ); + } + + /** + * Test purpose: Verify that actual database operations (save/retrieve) work correctly with overridden table names. + *
+ * Verification: + * 1. Verify that when saving Person, Company, Organization entities, data is stored in the overridden tables + * 2. Verify that saved data can be retrieved correctly + */ + @Test + public void testQueryExecution(SessionFactoryScope scope) { + // Test data persistence + scope.inTransaction( session -> { + // Persist Person entity + Person person = new Person(); + person.setName( "John Doe" ); + Address address = new Address(); + address.setStreet( "123 Main St" ); + address.setCity( "New York" ); + address.getPhones().add( "123-456-7890" ); + address.getPhones().add( "098-765-4321" ); + person.setAddress( address ); + session.persist( person ); + + // Persist Company entity + Company company = new Company(); + company.setName( "Acme Corp" ); + Address companyAddress = new Address(); + companyAddress.setStreet( "456 Business Ave" ); + companyAddress.setCity( "Boston" ); + companyAddress.getPhones().add( "555-123-4567" ); + company.setAddress( companyAddress ); + session.persist( company ); + + // Persist Organization entity + Organization organization = new Organization(); + organization.setName( "Tech Inc" ); + ContactInfo contactInfo = new ContactInfo(); + contactInfo.getEmails().add( "info@techinc.com" ); + contactInfo.getEmails().add( "support@techinc.com" ); + organization.setContactInfo( contactInfo ); + session.persist( organization ); + } ); + + // Test data retrieval + scope.inTransaction( session -> { + Person person = session.createQuery( "from Person", Person.class ).getSingleResult(); + assertThat( person.getName(), is( "John Doe" ) ); + assertThat( person.getAddress().getPhones().size(), is( 2 ) ); + + Company company = session.createQuery( "from Company", Company.class ).getSingleResult(); + assertThat( company.getName(), is( "Acme Corp" ) ); + assertThat( company.getAddress().getPhones().size(), is( 1 ) ); + + Organization organization = session.createQuery( "from Organization", Organization.class ) + .getSingleResult(); + assertThat( organization.getName(), is( "Tech Inc" ) ); + assertThat( organization.getContactInfo().getEmails().size(), is( 2 ) ); + } ); + } + + /** + * Test purpose: Verify that joinColumns and indexes from nested @CollectionTable + * in @CollectionTableOverride are correctly applied. + *
+ * Verification:
+ * 1. Verify that custom join column name is applied
+ * 2. Verify that indexes are correctly created on the collection table
+ */
+ @Test
+ public void testCollectionTableOverrideWithJoinColumnsAndIndexes(DomainModelScope scope) {
+ final MetadataImplementor metadata = scope.getDomainModel();
+
+ // Get the collection binding
+ final Collection customerTagsCollection = metadata.getCollectionBinding(
+ Customer.class.getName() + ".contact.tags"
+ );
+ assertNotNull( customerTagsCollection, "Collection should not be null" );
+
+ final org.hibernate.mapping.Table collectionTable = customerTagsCollection.getCollectionTable();
+ assertNotNull( collectionTable, "Collection table should not be null" );
+ assertEquals( "customer_tags", collectionTable.getName(), "Table name should be overridden" );
+
+ // Verify join column name is overridden by checking the key columns
+ @SuppressWarnings("unchecked")
+ java.util.Iterator