From aeb3aee62603f4e3613ed4037f2727c817a11016 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Fri, 21 Oct 2016 19:51:35 +0200 Subject: [PATCH] HHH-11180 - JPA @ForeignKey still not consistently applied from annotation binding - Fix ForeignKey support for PrimaryKeyJoinColumn / PrimaryKeyJoinColumns - Fix ForeignKey support for JoinColumn / JoinColumns - Fix ForeignKey support for JoinTable when applying value NO_CONSTRAINT. - Fix ForeignKey support for MapKeyJoinColumn / MapKeyJoinColumns - Fix ForeignKey support for AssociationOverride / AssociationOverrides --- .../hibernate/cfg/AbstractPropertyHolder.java | 171 +++--- .../org/hibernate/cfg/AnnotationBinder.java | 43 +- .../org/hibernate/cfg/PropertyHolder.java | 9 + .../cfg/annotations/CollectionBinder.java | 40 +- .../hibernate/cfg/annotations/MapBinder.java | 31 ++ .../constraint/ForeignKeyConstraintTest.java | 509 ++++++++++++++++++ 6 files changed, 716 insertions(+), 87 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/constraint/ForeignKeyConstraintTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java index 9edadbf2e5..5825f4d5c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java @@ -15,6 +15,7 @@ import javax.persistence.AttributeOverrides; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.Entity; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.MappedSuperclass; @@ -48,6 +49,8 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { private Map currentPropertyJoinColumnOverride; private Map holderJoinTableOverride; private Map currentPropertyJoinTableOverride; + private Map holderForeignKeyOverride; + private Map currentPropertyForeignKeyOverride; private String path; private MetadataBuildingContext context; private Boolean isInIdClass; @@ -170,29 +173,28 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { this.currentPropertyColumnOverride = null; this.currentPropertyJoinColumnOverride = null; this.currentPropertyJoinTableOverride = null; + this.currentPropertyForeignKeyOverride = null; } else { - this.currentPropertyColumnOverride = buildColumnOverride( - property, - getPath() - ); + this.currentPropertyColumnOverride = buildColumnOverride( property, getPath() ); if ( this.currentPropertyColumnOverride.size() == 0 ) { this.currentPropertyColumnOverride = null; } - this.currentPropertyJoinColumnOverride = buildJoinColumnOverride( - property, - getPath() - ); + + this.currentPropertyJoinColumnOverride = buildJoinColumnOverride( property, getPath() ); if ( this.currentPropertyJoinColumnOverride.size() == 0 ) { this.currentPropertyJoinColumnOverride = null; } - this.currentPropertyJoinTableOverride = buildJoinTableOverride( - property, - getPath() - ); + + this.currentPropertyJoinTableOverride = buildJoinTableOverride( property, getPath() ); if ( this.currentPropertyJoinTableOverride.size() == 0 ) { this.currentPropertyJoinTableOverride = null; } + + this.currentPropertyForeignKeyOverride = buildForeignKeyOverride( property, getPath() ); + if ( this.currentPropertyForeignKeyOverride.size() == 0 ) { + this.currentPropertyForeignKeyOverride = null; + } } } @@ -298,6 +300,30 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { return override; } + public ForeignKey getOverriddenForeignKey(String propertyName) { + ForeignKey result = getExactOverriddenForeignKey( propertyName ); + if ( result == null && propertyName.contains( ".collection&&element." ) ) { + //support for non map collections where no prefix is needed + //TODO cache the underlying regexp + result = getExactOverriddenForeignKey( propertyName.replace( ".collection&&element.", "." ) ); + } + return result; + } + + private ForeignKey getExactOverriddenForeignKey(String propertyName) { + ForeignKey override = null; + if ( parent != null ) { + override = parent.getExactOverriddenForeignKey( propertyName ); + } + if ( override == null && currentPropertyForeignKeyOverride != null ) { + override = currentPropertyForeignKeyOverride.get( propertyName ); + } + if ( override == null && holderForeignKeyOverride != null ) { + override = holderForeignKeyOverride.get( propertyName ); + } + return override; + } + /** * Get column overriding, property first, then parent, then holder * replace the placeholder 'collection&&element' with nothing @@ -352,6 +378,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { Map columnOverride = new HashMap(); Map joinColumnOverride = new HashMap(); Map joinTableOverride = new HashMap(); + Map foreignKeyOverride = new HashMap(); while ( current != null && !context.getBuildingOptions().getReflectionManager().toXClass( Object.class ).equals( current ) ) { if ( current.isAnnotationPresent( Entity.class ) || current.isAnnotationPresent( MappedSuperclass.class ) || current.isAnnotationPresent( Embeddable.class ) ) { @@ -359,12 +386,15 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { Map currentOverride = buildColumnOverride( current, getPath() ); Map currentJoinOverride = buildJoinColumnOverride( current, getPath() ); Map currentJoinTableOverride = buildJoinTableOverride( current, getPath() ); + Map currentForeignKeyOverride = buildForeignKeyOverride( current, getPath() ); currentOverride.putAll( columnOverride ); //subclasses have precedence over superclasses currentJoinOverride.putAll( joinColumnOverride ); //subclasses have precedence over superclasses currentJoinTableOverride.putAll( joinTableOverride ); //subclasses have precedence over superclasses + currentForeignKeyOverride.putAll( foreignKeyOverride ); //subclasses have precedence over superclasses columnOverride = currentOverride; joinColumnOverride = currentJoinOverride; joinTableOverride = currentJoinTableOverride; + foreignKeyOverride = currentForeignKeyOverride; } current = current.getSuperclass(); } @@ -372,31 +402,32 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { holderColumnOverride = columnOverride.size() > 0 ? columnOverride : null; holderJoinColumnOverride = joinColumnOverride.size() > 0 ? joinColumnOverride : null; holderJoinTableOverride = joinTableOverride.size() > 0 ? joinTableOverride : null; + holderForeignKeyOverride = foreignKeyOverride.size() > 0 ? foreignKeyOverride : null; } private static Map buildColumnOverride(XAnnotatedElement element, String path) { Map columnOverride = new HashMap(); - if ( element == null ) return columnOverride; - AttributeOverride singleOverride = element.getAnnotation( AttributeOverride.class ); - AttributeOverrides multipleOverrides = element.getAnnotation( AttributeOverrides.class ); - AttributeOverride[] overrides; - if ( singleOverride != null ) { - overrides = new AttributeOverride[] { singleOverride }; - } - else if ( multipleOverrides != null ) { - overrides = multipleOverrides.value(); - } - else { - overrides = null; - } + if ( element != null ) { + AttributeOverride singleOverride = element.getAnnotation( AttributeOverride.class ); + AttributeOverrides multipleOverrides = element.getAnnotation( AttributeOverrides.class ); + AttributeOverride[] overrides; + if ( singleOverride != null ) { + overrides = new AttributeOverride[]{ singleOverride }; + } + else if ( multipleOverrides != null ) { + overrides = multipleOverrides.value(); + } + else { + overrides = null; + } - //fill overridden columns - if ( overrides != null ) { - for (AttributeOverride depAttr : overrides) { - columnOverride.put( - StringHelper.qualify( path, depAttr.name() ), - new Column[] { depAttr.column() } - ); + if ( overrides != null ) { + for ( AttributeOverride depAttr : overrides ) { + columnOverride.put( + StringHelper.qualify( path, depAttr.name() ), + new Column[]{ depAttr.column() } + ); + } } } return columnOverride; @@ -404,56 +435,62 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { private static Map buildJoinColumnOverride(XAnnotatedElement element, String path) { Map columnOverride = new HashMap(); - if ( element == null ) return columnOverride; - AssociationOverride singleOverride = element.getAnnotation( AssociationOverride.class ); - AssociationOverrides multipleOverrides = element.getAnnotation( AssociationOverrides.class ); - AssociationOverride[] overrides; - if ( singleOverride != null ) { - overrides = new AssociationOverride[] { singleOverride }; - } - else if ( multipleOverrides != null ) { - overrides = multipleOverrides.value(); - } - else { - overrides = null; - } - - //fill overridden columns - if ( overrides != null ) { - for (AssociationOverride depAttr : overrides) { - columnOverride.put( - StringHelper.qualify( path, depAttr.name() ), - depAttr.joinColumns() - ); + if ( element != null ) { + AssociationOverride[] overrides = buildAssociationOverrides( element, path ); + if ( overrides != null ) { + for ( AssociationOverride depAttr : overrides ) { + columnOverride.put( + StringHelper.qualify( path, depAttr.name() ), + depAttr.joinColumns() + ); + } } } return columnOverride; } - private static Map buildJoinTableOverride(XAnnotatedElement element, String path) { - Map tableOverride = new HashMap(); - if ( element == null ) return tableOverride; + private static Map buildForeignKeyOverride(XAnnotatedElement element, String path) { + Map foreignKeyOverride = new HashMap(); + if ( element != null ) { + AssociationOverride[] overrides = buildAssociationOverrides( element, path ); + if ( overrides != null ) { + for ( AssociationOverride depAttr : overrides ) { + foreignKeyOverride.put( StringHelper.qualify( path, depAttr.name() ), depAttr.foreignKey() ); + } + } + } + return foreignKeyOverride; + } + + private static AssociationOverride[] buildAssociationOverrides(XAnnotatedElement element, String path) { AssociationOverride singleOverride = element.getAnnotation( AssociationOverride.class ); - AssociationOverrides multipleOverrides = element.getAnnotation( AssociationOverrides.class ); + AssociationOverrides pluralOverrides = element.getAnnotation( AssociationOverrides.class ); + AssociationOverride[] overrides; if ( singleOverride != null ) { overrides = new AssociationOverride[] { singleOverride }; } - else if ( multipleOverrides != null ) { - overrides = multipleOverrides.value(); + else if ( pluralOverrides != null ) { + overrides = pluralOverrides.value(); } else { overrides = null; } + return overrides; + } - //fill overridden tables - if ( overrides != null ) { - for (AssociationOverride depAttr : overrides) { - if ( depAttr.joinColumns().length == 0 ) { - tableOverride.put( - StringHelper.qualify( path, depAttr.name() ), - depAttr.joinTable() - ); + private static Map buildJoinTableOverride(XAnnotatedElement element, String path) { + Map tableOverride = new HashMap(); + if ( element != null ) { + AssociationOverride[] overrides = buildAssociationOverrides( element, path ); + if ( overrides != null ) { + for ( AssociationOverride depAttr : overrides ) { + if ( depAttr.joinColumns().length == 0 ) { + tableOverride.put( + StringHelper.qualify( path, depAttr.name() ), + depAttr.joinTable() + ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java index cce9ba4657..c05533dd9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -657,8 +657,20 @@ public final class AnnotationBinder { } else { final PrimaryKeyJoinColumn pkJoinColumn = clazzToProcess.getAnnotation( PrimaryKeyJoinColumn.class ); - if ( pkJoinColumn != null && pkJoinColumn.foreignKey() != null - && !StringHelper.isEmpty( pkJoinColumn.foreignKey().name() ) ) { + final PrimaryKeyJoinColumns pkJoinColumns = clazzToProcess.getAnnotation( PrimaryKeyJoinColumns.class ); + + if ( pkJoinColumns != null && pkJoinColumns.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { + // don't apply a constraint based on ConstraintMode + key.setForeignKeyName( "none" ); + } + else if ( pkJoinColumns != null && !StringHelper.isEmpty( pkJoinColumns.foreignKey().name() ) ) { + key.setForeignKeyName( pkJoinColumns.foreignKey().name() ); + } + else if ( pkJoinColumn != null && pkJoinColumn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { + // don't apply a constraint based on ConstraintMode + key.setForeignKeyName( "none" ); + } + else if ( pkJoinColumn != null && !StringHelper.isEmpty( pkJoinColumn.foreignKey().name() ) ) { key.setForeignKeyName( pkJoinColumn.foreignKey().name() ); } } @@ -2914,6 +2926,7 @@ public final class AnnotationBinder { } final JoinColumn joinColumn = property.getAnnotation( JoinColumn.class ); + final JoinColumns joinColumns = property.getAnnotation( JoinColumns.class ); //Make sure that JPA1 key-many-to-one columns are read only tooj boolean hasSpecjManyToOne=false; @@ -2942,8 +2955,8 @@ public final class AnnotationBinder { final String propertyName = inferredData.getPropertyName(); value.setTypeUsingReflection( propertyHolder.getClassName(), propertyName ); - if ( joinColumn != null - && joinColumn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { + if ( ( joinColumn != null && joinColumn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) + || ( joinColumns != null && joinColumns.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) ) { // not ideal... value.setForeignKeyName( "none" ); } @@ -2952,9 +2965,25 @@ public final class AnnotationBinder { if ( fk != null && StringHelper.isNotEmpty( fk.name() ) ) { value.setForeignKeyName( fk.name() ); } - else if ( joinColumn != null ) { - value.setForeignKeyName( StringHelper.nullIfEmpty( joinColumn.foreignKey().name() ) ); - value.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); + else { + final javax.persistence.ForeignKey fkOverride = propertyHolder.getOverriddenForeignKey( + StringHelper.qualify( propertyHolder.getPath(), propertyName ) + ); + if ( fkOverride != null && fkOverride.value() == ConstraintMode.NO_CONSTRAINT ) { + value.setForeignKeyName( "none" ); + } + else if ( fkOverride != null ) { + value.setForeignKeyName( StringHelper.nullIfEmpty( fkOverride.name() ) ); + value.setForeignKeyDefinition( StringHelper.nullIfEmpty( fkOverride.foreignKeyDefinition() ) ); + } + else if ( joinColumns != null ) { + value.setForeignKeyName( StringHelper.nullIfEmpty( joinColumns.foreignKey().name() ) ); + value.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumns.foreignKey().foreignKeyDefinition() ) ); + } + else if ( joinColumn != null ) { + value.setForeignKeyName( StringHelper.nullIfEmpty( joinColumn.foreignKey().name() ) ); + value.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java index 65404e3447..2c616a06bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java @@ -7,6 +7,7 @@ package org.hibernate.cfg; import javax.persistence.Column; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; @@ -67,6 +68,14 @@ public interface PropertyHolder { */ JoinColumn[] getOverriddenJoinColumn(String propertyName); + /** + * return null if hte foreign key is not overridden, or the foreign key if true + */ + default ForeignKey getOverriddenForeignKey(String propertyName) { + // todo: does this necessarily need to be a default method? + return null; + } + /** * return * - null if no join table is present, diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java index cdc8541a3b..55b984fb9b 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java @@ -868,7 +868,7 @@ public abstract class CollectionBinder { LOG.debugf( "Mapping collection: %s -> %s", collection.getRole(), collection.getCollectionTable().getName() ); } bindFilters( false ); - bindCollectionSecondPass( collection, null, fkJoinColumns, cascadeDeleteEnabled, property, buildingContext ); + bindCollectionSecondPass( collection, null, fkJoinColumns, cascadeDeleteEnabled, property, propertyHolder, buildingContext ); if ( !collection.isInverse() && !collection.getKey().isNullable() ) { // for non-inverse one-to-many, with a not-null fk, add a backref! @@ -1085,6 +1085,7 @@ public abstract class CollectionBinder { Ejb3JoinColumn[] joinColumns, boolean cascadeDeleteEnabled, XProperty property, + PropertyHolder propertyHolder, MetadataBuildingContext buildingContext) { //binding key reference using column KeyValue keyVal; @@ -1160,14 +1161,26 @@ public abstract class CollectionBinder { } } else { - final JoinColumn joinColumnAnn = property.getAnnotation( JoinColumn.class ); - if ( joinColumnAnn != null ) { - if ( joinColumnAnn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { - key.setForeignKeyName( "none" ); - } - else { - key.setForeignKeyName( StringHelper.nullIfEmpty( joinColumnAnn.foreignKey().name() ) ); - key.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumnAnn.foreignKey().foreignKeyDefinition() ) ); + final javax.persistence.ForeignKey fkOverride = propertyHolder.getOverriddenForeignKey( + StringHelper.qualify( propertyHolder.getPath(), property.getName() ) + ); + if ( fkOverride != null && fkOverride.value() == ConstraintMode.NO_CONSTRAINT ) { + key.setForeignKeyName( "none" ); + } + else if ( fkOverride != null ) { + key.setForeignKeyName( StringHelper.nullIfEmpty( fkOverride.name() ) ); + key.setForeignKeyDefinition( StringHelper.nullIfEmpty( fkOverride.foreignKeyDefinition() ) ); + } + else { + final JoinColumn joinColumnAnn = property.getAnnotation( JoinColumn.class ); + if ( joinColumnAnn != null ) { + if ( joinColumnAnn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { + key.setForeignKeyName( "none" ); + } + else { + key.setForeignKeyName( StringHelper.nullIfEmpty( joinColumnAnn.foreignKey().name() ) ); + key.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumnAnn.foreignKey().foreignKeyDefinition() ) ); + } } } } @@ -1319,7 +1332,7 @@ public abstract class CollectionBinder { collValue.setCollectionTable( associationTableBinder.bind() ); } bindFilters( isCollectionOfEntities ); - bindCollectionSecondPass( collValue, collectionEntity, joinColumns, cascadeDeleteEnabled, property, buildingContext ); + bindCollectionSecondPass( collValue, collectionEntity, joinColumns, cascadeDeleteEnabled, property, propertyHolder, buildingContext ); ManyToOne element = null; if ( isCollectionOfEntities ) { @@ -1348,7 +1361,7 @@ public abstract class CollectionBinder { if ( joinTableAnn != null ) { String foreignKeyName = joinTableAnn.inverseForeignKey().name(); String foreignKeyDefinition = joinTableAnn.inverseForeignKey().foreignKeyDefinition(); - ConstraintMode foreignKeyValue = joinTableAnn.foreignKey().value(); + ConstraintMode foreignKeyValue = joinTableAnn.inverseForeignKey().value(); if ( joinTableAnn.inverseJoinColumns().length != 0 ) { final JoinColumn joinColumnAnn = joinTableAnn.inverseJoinColumns()[0]; if ( "".equals( foreignKeyName ) ) { @@ -1359,7 +1372,7 @@ public abstract class CollectionBinder { foreignKeyValue = joinColumnAnn.foreignKey().value(); } } - if ( joinTableAnn.foreignKey().value() == ConstraintMode.NO_CONSTRAINT ) { + if ( joinTableAnn.inverseForeignKey().value() == ConstraintMode.NO_CONSTRAINT ) { element.setForeignKeyName( "none" ); } else { @@ -1575,6 +1588,7 @@ public abstract class CollectionBinder { Ejb3JoinColumn[] joinColumns, boolean cascadeDeleteEnabled, XProperty property, + PropertyHolder propertyHolder, MetadataBuildingContext buildingContext) { try { BinderHelper.createSyntheticPropertyReference( @@ -1589,7 +1603,7 @@ public abstract class CollectionBinder { catch (AnnotationException ex) { throw new AnnotationException( "Unable to map collection " + collValue.getOwner().getClassName() + "." + property.getName(), ex ); } - SimpleValue key = buildCollectionKey( collValue, joinColumns, cascadeDeleteEnabled, property, buildingContext ); + SimpleValue key = buildCollectionKey( collValue, joinColumns, cascadeDeleteEnabled, property, propertyHolder, buildingContext ); if ( property.isAnnotationPresent( ElementCollection.class ) && joinColumns.length > 0 ) { joinColumns[0].setJPA2ElementCollection( true ); } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java index 54c9fbcd96..876e864c6a 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java @@ -12,8 +12,11 @@ import java.util.Map; import java.util.Random; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; +import javax.persistence.ConstraintMode; import javax.persistence.InheritanceType; import javax.persistence.MapKeyClass; +import javax.persistence.MapKeyJoinColumn; +import javax.persistence.MapKeyJoinColumns; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; @@ -294,6 +297,20 @@ public class MapBinder extends CollectionBinder { col.forceNotNull(); } } + + if ( element != null ) { + final javax.persistence.ForeignKey foreignKey = getMapKeyForeignKey( property ); + if ( foreignKey != null ) { + if ( foreignKey.value() == ConstraintMode.NO_CONSTRAINT ) { + element.setForeignKeyName( "none" ); + } + else { + element.setForeignKeyName( StringHelper.nullIfEmpty( foreignKey.name() ) ); + element.setForeignKeyDefinition( StringHelper.nullIfEmpty( foreignKey.foreignKeyDefinition() ) ); + } + } + } + if ( isIndexOfEntities ) { bindManytoManyInverseFk( collectionEntity, @@ -306,6 +323,20 @@ public class MapBinder extends CollectionBinder { } } + private javax.persistence.ForeignKey getMapKeyForeignKey(XProperty property) { + final MapKeyJoinColumns mapKeyJoinColumns = property.getAnnotation( MapKeyJoinColumns.class ); + if ( mapKeyJoinColumns != null ) { + return mapKeyJoinColumns.foreignKey(); + } + else { + final MapKeyJoinColumn mapKeyJoinColumn = property.getAnnotation( MapKeyJoinColumn.class ); + if ( mapKeyJoinColumn != null ) { + return mapKeyJoinColumn.foreignKey(); + } + } + return null; + } + private boolean mappingDefinedAttributeOverrideOnMapKey(XProperty property) { if ( property.isAnnotationPresent( AttributeOverride.class ) ) { return namedMapKey( property.getAnnotation( AttributeOverride.class ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/test/constraint/ForeignKeyConstraintTest.java b/hibernate-core/src/test/java/org/hibernate/test/constraint/ForeignKeyConstraintTest.java new file mode 100644 index 0000000000..6ce80c6f5e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/constraint/ForeignKeyConstraintTest.java @@ -0,0 +1,509 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.constraint; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.AssociationOverride; +import javax.persistence.AssociationOverrides; +import javax.persistence.CollectionTable; +import javax.persistence.ConstraintMode; +import javax.persistence.ElementCollection; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.MapKeyJoinColumn; +import javax.persistence.MapKeyJoinColumns; +import javax.persistence.MappedSuperclass; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.PrimaryKeyJoinColumns; +import javax.persistence.SecondaryTable; + +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.mapping.Column; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Christian Beikov + */ +@TestForIssue( jiraKey = "HHH-11180" ) +public class ForeignKeyConstraintTest extends BaseNonConfigCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + CreditCard.class, + Person.class, + Student.class, + Professor.class, + Vehicle.class, + VehicleBuyInfo.class, + Car.class, + Truck.class, + Company.class + }; + } + + @Test + public void testJoinColumn() { + assertForeignKey( "FK_CAR_OWNER", "OWNER_PERSON_ID" ); + assertForeignKey( "FK_CAR_OWNER3", "OWNER_PERSON_ID3" ); + assertForeignKey( "FK_PERSON_CC", "PERSON_CC_ID" ); + assertNoForeignKey( "FK_CAR_OWNER2", "OWNER_PERSON_ID2" ); + assertNoForeignKey( "FK_CAR_OWNER4", "OWNER_PERSON_ID4" ); + assertNoForeignKey( "FK_PERSON_CC2", "PERSON_CC_ID2" ); + } + + @Test + public void testJoinColumns() { + assertForeignKey( "FK_STUDENT_CAR", "CAR_NR", "CAR_VENDOR_NR" ); + assertForeignKey( "FK_STUDENT_CAR3", "CAR_NR3", "CAR_VENDOR_NR3" ); + assertNoForeignKey( "FK_STUDENT_CAR2", "CAR_NR2", "CAR_VENDOR_NR2" ); + assertNoForeignKey( "FK_STUDENT_CAR4", "CAR_NR4", "CAR_VENDOR_NR4" ); + } + + @Test + public void testJoinTable() { + assertForeignKey( "FK_VEHICLE_BUY_INFOS_STUDENT", "STUDENT_ID" ); + } + + @Test + public void testJoinTableInverse() { + assertForeignKey( "FK_VEHICLE_BUY_INFOS_VEHICLE_BUY_INFO", "VEHICLE_BUY_INFO_ID" ); + } + + @Test + public void testPrimaryKeyJoinColumn() { + assertForeignKey( "FK_STUDENT_PERSON", "PERSON_ID" ); + assertNoForeignKey( "FK_PROFESSOR_PERSON", "PERSON_ID" ); + } + + @Test + public void testPrimaryKeyJoinColumns() { + assertForeignKey( "FK_CAR_VEHICLE", "CAR_NR", "VENDOR_NR" ); + assertNoForeignKey( "FK_TRUCK_VEHICLE", "CAR_NR", "VENDOR_NR" ); + } + + @Test + public void testCollectionTable() { + assertForeignKey( "FK_OWNER_INFO_CAR", "CAR_NR", "VENDOR_NR" ); + } + + @Test + public void testMapKeyJoinColumn() { + assertForeignKey( "FK_OWNER_INFO_PERSON", "PERSON_ID" ); + } + + @Test + public void testMapKeyJoinColumns() { + assertForeignKey( "FK_VEHICLE_BUY_INFOS_VEHICLE", "VEHICLE_NR", "VEHICLE_VENDOR_NR" ); + } + + @Test + public void testAssociationOverride() { + // class level association overrides + assertForeignKey( "FK_COMPANY_OWNER", "OWNER_PERSON_ID" ); + assertForeignKey( "FK_COMPANY_CREDIT_CARD", "CREDIT_CARD_ID" ); + assertForeignKey( "FK_COMPANY_CREDIT_CARD3", "CREDIT_CARD_ID3" ); + assertNoForeignKey( "FK_COMPANY_OWNER2", "OWNER_PERSON_ID2" ); + assertNoForeignKey( "FK_COMPANY_CREDIT_CARD2", "CREDIT_CARD_ID2" ); + assertNoForeignKey( "FK_COMPANY_CREDIT_CARD4", "CREDIT_CARD_ID4" ); + + // embeddable association overrides + assertForeignKey( "FK_COMPANY_CARD", "AO_CI_CC_ID" ); + assertNoForeignKey( "FK_COMPANY_CARD2", "AO_CI_CC_ID2" ); + assertForeignKey( "FK_COMPANY_CARD3", "AO_CI_CC_ID3" ); + assertNoForeignKey( "FK_COMPANY_CARD4", "AO_CI_CC_ID4" ); + } + + @Test + public void testSecondaryTable() { + assertForeignKey( "FK_CAR_DETAILS_CAR", "CAR_NR", "CAR_VENDOR_NR" ); + } + + private void assertForeignKey(String foreignKeyName, String... columns) { + Set columnSet = new LinkedHashSet<>( Arrays.asList( columns ) ); + for ( Namespace namespace : metadata().getDatabase().getNamespaces() ) { + for ( org.hibernate.mapping.Table table : namespace.getTables() ) { + Iterator fkItr = table.getForeignKeyIterator(); + while ( fkItr.hasNext() ) { + org.hibernate.mapping.ForeignKey fk = fkItr.next(); + + if ( foreignKeyName.equals( fk.getName() ) ) { + assertEquals( "ForeignKey column count not like expected", columnSet.size(), fk.getColumnSpan() ); + List columnNames = fk.getColumns().stream().map(Column::getName).collect(Collectors.toList()); + assertTrue( + "ForeignKey columns [" + columnNames + "] do not match expected columns [" + columnSet + "]", + columnSet.containsAll( columnNames ) + ); + return; + } + } + } + } + fail( "ForeignKey '" + foreignKeyName + "' could not be found!" ); + } + + private void assertNoForeignKey(String foreignKeyName, String... columns) { + Set columnSet = new LinkedHashSet<>( Arrays.asList( columns ) ); + for ( Namespace namespace : metadata().getDatabase().getNamespaces() ) { + for ( org.hibernate.mapping.Table table : namespace.getTables() ) { + Iterator fkItr = table.getForeignKeyIterator(); + while ( fkItr.hasNext() ) { + org.hibernate.mapping.ForeignKey fk = fkItr.next(); + assertFalse( + "ForeignKey [" + foreignKeyName + "] defined and shouldn't have been.", + foreignKeyName.equals( fk.getName() ) + ); + } + } + } + } + + @Entity(name = "CreditCard") + public static class CreditCard { + @Id + public String number; + } + + @Entity(name = "Person") + @Inheritance( strategy = InheritanceType.JOINED ) + public static class Person { + @Id + @GeneratedValue + @javax.persistence.Column( nullable = false, unique = true) + public long id; + + @OneToMany + @JoinColumn(name = "PERSON_CC_ID", foreignKey = @ForeignKey( name = "FK_PERSON_CC" ) ) + public List creditCards; + + @OneToMany + @JoinColumn(name = "PERSON_CC_ID2", foreignKey = @ForeignKey( name = "FK_PERSON_CC2", value = ConstraintMode.NO_CONSTRAINT ) ) + public List creditCards2; + } + + @Entity(name = "Professor") + @PrimaryKeyJoinColumn( + name = "PERSON_ID", + foreignKey = @ForeignKey( name = "FK_PROFESSOR_PERSON", value = ConstraintMode.NO_CONSTRAINT ) + ) + public static class Professor extends Person { + + } + + @Entity(name = "Student") + @PrimaryKeyJoinColumn( name = "PERSON_ID", foreignKey = @ForeignKey( name = "FK_STUDENT_PERSON" ) ) + public static class Student extends Person { + + @javax.persistence.Column( name = "MATRICULATION_NUMBER" ) + public String matriculationNumber; + + @ManyToOne + @JoinColumns( + value = { + @JoinColumn( name = "CAR_NR", referencedColumnName = "CAR_NR" ), + @JoinColumn( name = "CAR_VENDOR_NR", referencedColumnName = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_STUDENT_CAR" ) + ) + public Car car; + + @ManyToOne + @JoinColumns( + value = { + @JoinColumn( name = "CAR_NR2", referencedColumnName = "CAR_NR" ), + @JoinColumn( name = "CAR_VENDOR_NR2", referencedColumnName = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_STUDENT_CAR2", value = ConstraintMode.NO_CONSTRAINT ) + ) + public Car car2; + + @OneToOne + @JoinColumns( + value = { + @JoinColumn( name = "CAR_NR3", referencedColumnName = "CAR_NR" ), + @JoinColumn( name = "CAR_VENDOR_NR3", referencedColumnName = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_STUDENT_CAR3" ) + ) + public Car car3; + + @OneToOne + @JoinColumns( + value = { + @JoinColumn( name = "CAR_NR4", referencedColumnName = "CAR_NR" ), + @JoinColumn( name = "CAR_VENDOR_NR4", referencedColumnName = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_STUDENT_CAR4", value = ConstraintMode.NO_CONSTRAINT ) + ) + public Car car4; + + @ManyToMany + @JoinTable( + name = "VEHICLE_BUY_INFOS", + foreignKey = @ForeignKey( name = "FK_VEHICLE_BUY_INFOS_STUDENT" ), + inverseForeignKey = @ForeignKey( name = "FK_VEHICLE_BUY_INFOS_VEHICLE_BUY_INFO" ), + joinColumns = @JoinColumn( name = "STUDENT_ID"), + inverseJoinColumns = @JoinColumn( name = "VEHICLE_BUY_INFO_ID" ) + ) + @MapKeyJoinColumns( + value = { + @MapKeyJoinColumn( name = "VEHICLE_NR", referencedColumnName = "VEHICLE_NR" ), + @MapKeyJoinColumn( name = "VEHICLE_VENDOR_NR", referencedColumnName = "VEHICLE_VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_VEHICLE_BUY_INFOS_VEHICLE" ) + ) + public Map vehicleBuyInfos = new HashMap<>(); + } + + @Entity(name = "VehicleBuyInfo") + public static class VehicleBuyInfo { + @Id + @GeneratedValue + public long id; + public String info; + } + + @Entity(name = "Vehicle") + @Inheritance( strategy = InheritanceType.JOINED ) + public static class Vehicle { + @EmbeddedId + public VehicleId id; + } + + @Embeddable + public static class VehicleId implements Serializable { + @javax.persistence.Column( name = "VEHICLE_VENDOR_NR" ) + public long vehicleVendorNumber; + @javax.persistence.Column( name = "VEHICLE_NR" ) + public long vehicleNumber; + + @Override + public boolean equals(Object o) { + if ( this == o ) return true; + if ( !( o instanceof VehicleId ) ) return false; + + VehicleId vehicleId = (VehicleId) o; + + if ( vehicleVendorNumber != vehicleId.vehicleVendorNumber ) return false; + return vehicleNumber == vehicleId.vehicleNumber; + } + + @Override + public int hashCode() { + int result = (int) ( vehicleVendorNumber ^ ( vehicleVendorNumber >>> 32 ) ); + result = 31 * result + (int) ( vehicleNumber ^ ( vehicleNumber >>> 32 ) ); + return result; + } + } + + @Entity(name = "Car") + @SecondaryTable( + name = "CAR_DETAILS", + pkJoinColumns = { + @PrimaryKeyJoinColumn( name = "CAR_NR", referencedColumnName = "CAR_NR" ), + @PrimaryKeyJoinColumn( name = "CAR_VENDOR_NR", referencedColumnName = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_CAR_DETAILS_CAR" ) + ) + @PrimaryKeyJoinColumns( + value = { + @PrimaryKeyJoinColumn( name = "CAR_NR", referencedColumnName = "VEHICLE_NR" ), + @PrimaryKeyJoinColumn( name = "VENDOR_NR", referencedColumnName = "VEHICLE_VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_CAR_VEHICLE" ) + ) + public static class Car extends Vehicle { + + public String color; + + @ManyToOne + @JoinColumn( name = "OWNER_PERSON_ID", foreignKey = @ForeignKey( name = "FK_CAR_OWNER") ) + public Person owner; + + @ManyToOne + @JoinColumn( name = "OWNER_PERSON_ID2", foreignKey = @ForeignKey( name = "FK_CAR_OWNER2", value = ConstraintMode.NO_CONSTRAINT ) ) + public Person owner2; + + @OneToOne + @JoinColumn( name = "OWNER_PERSON_ID3", foreignKey = @ForeignKey( name = "FK_CAR_OWNER3") ) + public Person owner3; + + @OneToOne + @JoinColumn( name = "OWNER_PERSON_ID4", foreignKey = @ForeignKey( name = "FK_CAR_OWNER4", value = ConstraintMode.NO_CONSTRAINT ) ) + public Person owner4; + + @ElementCollection + @CollectionTable( + name = "OWNER_INFO", + joinColumns = { + @JoinColumn( name = "CAR_NR" ), + @JoinColumn( name = "VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_OWNER_INFO_CAR" ) + ) + @MapKeyJoinColumn( name = "PERSON_ID", foreignKey = @ForeignKey( name = "FK_OWNER_INFO_PERSON" ) ) + public Map ownerInfo = new HashMap<>(); + + } + + @Entity(name = "Truck") + @PrimaryKeyJoinColumns( + value = { + @PrimaryKeyJoinColumn( name = "CAR_NR", referencedColumnName = "VEHICLE_NR" ), + @PrimaryKeyJoinColumn( name = "VENDOR_NR", referencedColumnName = "VEHICLE_VENDOR_NR" ) + }, + foreignKey = @ForeignKey( name = "FK_TRUCK_VEHICLE", value = ConstraintMode.NO_CONSTRAINT ) + ) + public static class Truck extends Vehicle { + public boolean fourWheelDrive; + } + + @MappedSuperclass + public static abstract class AbstractCompany { + + @Id + @GeneratedValue + public long id; + + @ManyToOne + @JoinColumn( name = "OWNER_ID" ) + public Person owner; + + @ManyToOne + @JoinColumn( name = "OWNER_ID2" ) + public Person owner2; + + @OneToOne + @JoinColumn( name = "CC_ID" ) + public CreditCard creditCard; + + @OneToOne + @JoinColumn( name = "CC_ID2" ) + public CreditCard creditCard2; + + @OneToMany + @JoinColumn( name = "CC_ID3" ) + public List creditCards1; + + @OneToMany + @JoinColumn( name = "CC_ID4" ) + public List creditCards2; + } + + @Embeddable + public static class CompanyInfo { + public String data; + + @OneToMany + @JoinColumn( name = "CI_CC_ID", foreignKey = @ForeignKey( name = "FK_CI_CC" ) ) + public List cards; + + @OneToMany + @JoinColumn( name = "CI_CC_ID2", foreignKey = @ForeignKey( name = "FK_CI_CC2" ) ) + public List cards2; + + @ManyToOne + @JoinColumn( name = "CI_CC_ID3", foreignKey = @ForeignKey( name = "FK_CI_CC3" ) ) + public CreditCard cards3; + + @ManyToOne + @JoinColumn( name = "CI_CC_ID4", foreignKey = @ForeignKey( name = "FK_CI_CC4" ) ) + public CreditCard cards4; + } + + @Entity(name = "Company") + @AssociationOverrides({ + @AssociationOverride( + name = "owner", + joinColumns = @JoinColumn( name = "OWNER_PERSON_ID" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_OWNER" ) + ), + @AssociationOverride( + name = "owner2", + joinColumns = @JoinColumn( name = "OWNER_PERSON_ID2" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_OWNER2", value = ConstraintMode.NO_CONSTRAINT ) + ), + @AssociationOverride( + name = "creditCard", + joinColumns = @JoinColumn( name = "CREDIT_CARD_ID" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CREDIT_CARD" ) + ), + @AssociationOverride( + name = "creditCard2", + joinColumns = @JoinColumn( name = "CREDIT_CARD_ID2" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CREDIT_CARD2", value = ConstraintMode.NO_CONSTRAINT ) + ), + @AssociationOverride( + name = "creditCards1", + joinColumns = @JoinColumn( name = "CREDIT_CARD_ID3" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CREDIT_CARD3" ) + ), + @AssociationOverride( + name = "creditCards2", + joinColumns = @JoinColumn( name = "CREDIT_CARD_ID4" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CREDIT_CARD4", value = ConstraintMode.NO_CONSTRAINT ) + ) + + }) + public static class Company extends AbstractCompany { + @Embedded + @AssociationOverrides({ + @AssociationOverride( + name = "cards", + joinColumns = @JoinColumn( name = "AO_CI_CC_ID" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CARD" ) + ), + @AssociationOverride( + name = "cards2", + joinColumns = @JoinColumn( name = "AO_CI_CC_ID2" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CARD2", value = ConstraintMode.NO_CONSTRAINT ) + ), + @AssociationOverride( + name = "cards3", + joinColumns = @JoinColumn( name = "AO_CI_CC_ID3" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CARD3" ) + ), + @AssociationOverride( + name = "cards4", + joinColumns = @JoinColumn( name = "AO_CI_CC_ID4" ), + foreignKey = @ForeignKey( name = "FK_COMPANY_CARD4", value = ConstraintMode.NO_CONSTRAINT ) + ) + }) + public CompanyInfo info; + } +}