clean up foreign key binding code

This commit is contained in:
Gavin King 2022-10-27 16:16:04 +02:00
parent 264d3c711c
commit 239fc9a835
8 changed files with 232 additions and 211 deletions

View File

@ -6,10 +6,9 @@
*/
package org.hibernate.cfg;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.PrimaryKeyJoinColumn;
@ -779,73 +778,60 @@ public class AnnotatedJoinColumn extends AnnotatedColumn {
AnnotatedJoinColumn[] columns,
PersistentClass referencedEntity,
MetadataBuildingContext context) {
//convenient container to find whether a column is an id one or not
Set<Column> idColumns = new HashSet<>();
for ( Selectable selectable : referencedEntity.getKey().getSelectables() ) {
idColumns.add( (Column) selectable );
if ( columns.length == 0 ) {
return NO_REFERENCE; //shortcut
}
boolean isFkReferencedColumnName = false;
boolean noReferencedColumn = true;
//build the list of potential tables
if ( columns.length == 0 ) return NO_REFERENCE; //shortcut
Object columnOwner = findColumnOwner( referencedEntity, columns[0].getReferencedColumn(), context );
final Object columnOwner = findColumnOwner( referencedEntity, columns[0].getReferencedColumn(), context );
if ( columnOwner == null ) {
try {
throw new MappingException(
"No column with logical name '"
+ columns[0].getReferencedColumn()
+ "' in table '" + referencedEntity.getTable().getName()
+ "' for entity '" + referencedEntity.getEntityName()
+ "', nor in its related superclass tables and secondary tables"
"A '@JoinColumn' references a column named '" + columns[0].getReferencedColumn()
+ "' but the target entity '" + referencedEntity.getEntityName()
+ "' has no property which maps to this column"
);
}
catch (MappingException e) {
throw new RecoverableException( e.getMessage(), e );
}
}
Table matchingTable = columnOwner instanceof PersistentClass ?
( (PersistentClass) columnOwner ).getTable() :
( (Join) columnOwner ).getTable();
//check each referenced column
for (AnnotatedJoinColumn ejb3Column : columns) {
String logicalReferencedColumnName = ejb3Column.getReferencedColumn();
final Table table = columnOwner instanceof PersistentClass
? ( (PersistentClass) columnOwner ).getTable()
: ( (Join) columnOwner ).getTable();
final List<Selectable> keyColumns = referencedEntity.getKey().getSelectables();
boolean explicitColumnReference = false;
for ( AnnotatedJoinColumn column : columns ) {
final String logicalReferencedColumnName = column.getReferencedColumn();
if ( StringHelper.isNotEmpty( logicalReferencedColumnName ) ) {
String referencedColumnName;
try {
referencedColumnName = context.getMetadataCollector().getPhysicalColumnName(
matchingTable,
logicalReferencedColumnName
);
}
catch (MappingException me) {
//rewrite the exception
throw new MappingException(
"No column with logical name '" + logicalReferencedColumnName
+ "' in table '" + matchingTable.getName() + "'"
);
}
noReferencedColumn = false;
Column refCol = new Column( referencedColumnName );
boolean contains = idColumns.contains( refCol );
if ( !contains ) {
isFkReferencedColumnName = true;
break; //we know the state
explicitColumnReference = true;
if ( !keyColumns.contains( column( context, table, logicalReferencedColumnName ) ) ) {
// we have a column which does not belong to the PK
return NON_PK_REFERENCE;
}
}
}
if ( isFkReferencedColumnName ) {
return NON_PK_REFERENCE;
}
else if ( noReferencedColumn ) {
return NO_REFERENCE;
}
else if ( idColumns.size() != columns.length ) {
//reference use PK but is a subset or a superset
return NON_PK_REFERENCE;
if ( explicitColumnReference ) {
// if we got to here, all the columns belong to the PK
return keyColumns.size() == columns.length
// we have all the PK columns
? PK_REFERENCE
// we have a subset of the PK columns
: NON_PK_REFERENCE;
}
else {
return PK_REFERENCE;
// there were no nonempty referencedColumnNames
return NO_REFERENCE;
}
}
private static Column column(MetadataBuildingContext context, Table table, String logicalReferencedColumnName) {
try {
return new Column( context.getMetadataCollector().getPhysicalColumnName( table, logicalReferencedColumnName ) );
}
catch (MappingException me) {
throw new MappingException( "No column with logical name '" + logicalReferencedColumnName
+ "' in table '" + table.getName() + "'" );
}
}

View File

@ -84,6 +84,7 @@ import static org.hibernate.internal.util.StringHelper.isEmpty;
import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.EMBEDDED;
import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.NOOP;
import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.interpret;
/**
* @author Emmanuel Bernard
*/

View File

@ -41,6 +41,6 @@ public class JoinedSubclassFkSecondPass extends FkSecondPass {
}
public void doSecondPass(Map<String, PersistentClass> persistentClasses) throws MappingException {
TableBinder.bindFk( entity.getSuperclass(), entity, columns, value, false, buildingContext );
TableBinder.bindForeignKey( entity.getSuperclass(), entity, columns, value, false, buildingContext );
}
}

View File

@ -98,7 +98,7 @@ public class ToOneFkSecondPass extends FkSecondPass {
}
manyToOne.setPropertyName( path );
createSyntheticPropertyReference( columns, ref, null, manyToOne, false, buildingContext );
TableBinder.bindFk( ref, null, columns, manyToOne, unique, buildingContext );
TableBinder.bindForeignKey( ref, null, columns, manyToOne, unique, buildingContext );
/*
* HbmMetadataSourceProcessorImpl does this only when property-ref != null, but IMO, it makes sense event if it is null
*/

View File

@ -2653,7 +2653,7 @@ public abstract class CollectionBinder {
if ( property.isAnnotationPresent( ElementCollection.class ) && joinColumns.length > 0 ) {
joinColumns[0].setJPA2ElementCollection( true );
}
TableBinder.bindFk( collValue.getOwner(), collectionEntity, joinColumns, key, false, buildingContext );
TableBinder.bindForeignKey( collValue.getOwner(), collectionEntity, joinColumns, key, false, buildingContext );
key.sortProperties();
}
@ -2703,7 +2703,7 @@ public abstract class CollectionBinder {
if ( notFoundAction == NotFoundAction.IGNORE ) {
value.disableForeignKey();
}
TableBinder.bindFk( referencedEntity, null, columns, value, unique, buildingContext );
TableBinder.bindForeignKey( referencedEntity, null, columns, value, unique, buildingContext );
}
}

View File

@ -1813,7 +1813,7 @@ public class EntityBinder {
join.setKey( key );
setFKNameIfDefined( join );
key.setCascadeDeleteEnabled( false );
TableBinder.bindFk( persistentClass, null, annotatedJoinColumns, key, false, buildingContext );
TableBinder.bindForeignKey( persistentClass, null, annotatedJoinColumns, key, false, buildingContext );
key.sortProperties();
join.createPrimaryKey();
join.createForeignKey();

View File

@ -26,6 +26,7 @@ import org.hibernate.cfg.AnnotatedJoinColumn;
import org.hibernate.cfg.IndexOrUniqueKeySecondPass;
import org.hibernate.cfg.JPAIndexHolder;
import org.hibernate.cfg.ObjectNameSource;
import org.hibernate.cfg.PropertyHolder;
import org.hibernate.cfg.UniqueConstraintHolder;
import org.hibernate.dialect.Dialect;
import org.hibernate.internal.CoreMessageLogger;
@ -46,6 +47,9 @@ import org.hibernate.mapping.Value;
import org.jboss.logging.Logger;
import static org.hibernate.cfg.AnnotatedJoinColumn.NON_PK_REFERENCE;
import static org.hibernate.cfg.AnnotatedJoinColumn.NO_REFERENCE;
import static org.hibernate.cfg.AnnotatedJoinColumn.checkReferencedColumnsType;
import static org.hibernate.cfg.BinderHelper.isEmptyOrNullAnnotationValue;
import static org.hibernate.internal.util.StringHelper.isNotEmpty;
import static org.hibernate.internal.util.StringHelper.isQuoted;
@ -525,7 +529,7 @@ public class TableBinder {
return table;
}
public static void bindFk(
public static void bindForeignKey(
PersistentClass referencedEntity,
PersistentClass destinationEntity,
AnnotatedJoinColumn[] columns,
@ -538,163 +542,23 @@ public class TableBinder {
associatedClass = destinationEntity;
}
else {
associatedClass = columns[0].getPropertyHolder() == null
? null
: columns[0].getPropertyHolder().getPersistentClass();
PropertyHolder holder = columns[0].getPropertyHolder();
associatedClass = holder == null ? null : holder.getPersistentClass();
}
final String mappedByProperty = columns[0].getMappedBy();
if ( isNotEmpty( mappedByProperty ) ) {
// Get the columns of the mapped-by property
// use the columns of the property referenced by mappedBy
// copy them and link the copy to the actual value
LOG.debugf( "Retrieving property %s.%s", associatedClass.getEntityName(), mappedByProperty );
final Value propertyVal = associatedClass.getRecursiveProperty( columns[0].getMappedBy() ).getValue();
final List<Column> mappedByColumns;
if ( propertyVal instanceof Collection ) {
Value element = ((Collection) propertyVal).getElement();
if ( element == null ) {
throw new AnnotationException(
"Both sides of the bidirectional association '"
+ associatedClass.getEntityName() + "." + mappedByProperty
+ "' specify 'mappedBy'"
);
}
mappedByColumns = element.getColumns();
}
else {
mappedByColumns = propertyVal.getColumns();
}
for ( Column column: mappedByColumns ) {
columns[0].overrideFromReferencedColumnIfNecessary( column );
columns[0].linkValueUsingAColumnCopy( column, value );
}
bindUnownedAssociation( columns, value, associatedClass, mappedByProperty );
}
else if ( columns[0].isImplicit() ) {
// if columns are implicit, then create the columns based on the
// referenced entity id columns
List<Column> idColumns = referencedEntity instanceof JoinedSubclass
? referencedEntity.getKey().getColumns()
: referencedEntity.getIdentifier().getColumns();
for ( Column column: idColumns ) {
columns[0].linkValueUsingDefaultColumnNaming( column, referencedEntity, value );
columns[0].overrideFromReferencedColumnIfNecessary( column );
}
// if columns are implicit, then create the columns based
// on the referenced entity id columns
bindImplicitColumns( referencedEntity, columns, value );
}
else {
int fkEnum = AnnotatedJoinColumn.checkReferencedColumnsType( columns, referencedEntity, buildingContext );
if ( AnnotatedJoinColumn.NON_PK_REFERENCE == fkEnum ) {
String referencedPropertyName;
if ( value instanceof ToOne ) {
referencedPropertyName = ( (ToOne) value ).getReferencedPropertyName();
}
else if ( value instanceof DependantValue ) {
String propertyName = columns[0].getPropertyName();
if ( propertyName != null ) {
Collection collection = (Collection)
referencedEntity.getRecursiveProperty( propertyName ).getValue();
referencedPropertyName = collection.getReferencedPropertyName();
}
else {
throw new AnnotationException( "The '@JoinColumn' for a secondary table must reference the primary key" );
}
}
else {
throw new AssertionFailure(
"Do a property ref on an unexpected Value type: "
+ value.getClass().getName()
);
}
if ( referencedPropertyName == null ) {
throw new AssertionFailure(
"No property ref found while expected"
);
}
Property synthProp = referencedEntity.getReferencedProperty( referencedPropertyName );
if ( synthProp == null ) {
throw new AssertionFailure(
"Cannot find synthProp: " + referencedEntity.getEntityName() + "." + referencedPropertyName
);
}
linkJoinColumnWithValueOverridingNameIfImplicit( referencedEntity, synthProp.getValue(), columns, value );
if ( value instanceof SortableValue ) {
( (SortableValue) value ).sortProperties();
}
}
else {
if ( AnnotatedJoinColumn.NO_REFERENCE == fkEnum ) {
//implicit case, we hope PK and FK columns are in the same order
if ( columns.length != referencedEntity.getIdentifier().getColumnSpan() ) {
throw new AnnotationException(
"A foreign key that references '" + referencedEntity.getEntityName()
+ "' from entity '" + associatedClass.getEntityName()
+ "' has " + columns.length + " columns but the primary key has "
+ referencedEntity.getIdentifier().getColumnSpan() + " columns"
);
}
linkJoinColumnWithValueOverridingNameIfImplicit(
referencedEntity,
referencedEntity.getIdentifier(),
columns,
value
);
if ( value instanceof SortableValue ) {
( (SortableValue) value ).sortProperties();
}
}
else {
// Ensure the component is sorted so that we can simply set sorted to true on the to-one
KeyValue key = referencedEntity.getKey();
if ( key instanceof Component ) {
( (Component) key).sortProperties();
}
//explicit referencedColumnName
List<Column> idColumns = key.getColumns();
//works cause the pk has to be on the primary table
if ( idColumns.isEmpty() ) {
LOG.debug( "No column in the identifier" );
}
final Dialect dialect = buildingContext.getMetadataCollector().getDatabase()
.getJdbcEnvironment().getDialect();
for ( Column col: idColumns ) {
boolean match = false;
//for each PK column, find the associated FK column.
final String colName = col.getQuotedName(dialect);
for ( AnnotatedJoinColumn joinCol : columns ) {
String referencedColumn = joinCol.getReferencedColumn();
referencedColumn = buildingContext.getMetadataCollector().getPhysicalColumnName(
referencedEntity.getTable(),
referencedColumn
);
//In JPA 2 referencedColumnName is case-insensitive
if ( referencedColumn.equalsIgnoreCase( colName ) ) {
//proper join column
if ( joinCol.isNameDeferred() ) {
joinCol.linkValueUsingDefaultColumnNaming(
col, referencedEntity, value
);
}
else {
joinCol.linkWithValue( value );
}
joinCol.overrideFromReferencedColumnIfNecessary( col );
match = true;
break;
}
}
if ( !match ) {
throw new AnnotationException(
"Column name " + col.getName() + " of "
+ referencedEntity.getEntityName() + " not found in JoinColumns.referencedColumnName"
);
}
}
if ( value instanceof ToOne ) {
( (ToOne) value ).setSorted( true );
}
}
}
bindExplicitColumns( referencedEntity, columns, value, buildingContext, associatedClass );
}
value.createForeignKey();
if ( unique ) {
@ -702,15 +566,186 @@ public class TableBinder {
}
}
private static void bindExplicitColumns(
PersistentClass referencedEntity,
AnnotatedJoinColumn[] columns,
SimpleValue value,
MetadataBuildingContext buildingContext,
PersistentClass associatedClass) {
switch ( checkReferencedColumnsType( columns, referencedEntity, buildingContext ) ) {
case NON_PK_REFERENCE: {
bindNonPkReference( referencedEntity, columns, value );
break;
}
case NO_REFERENCE: {
bindImplicitPkReference( referencedEntity, columns, value, associatedClass );
break;
}
default: {
bindPkReference( referencedEntity, columns, value, associatedClass, buildingContext );
}
}
}
private static void bindImplicitPkReference(
PersistentClass referencedEntity,
AnnotatedJoinColumn[] columns,
SimpleValue value,
PersistentClass associatedClass) {
//implicit case, we hope PK and FK columns are in the same order
if ( columns.length != referencedEntity.getIdentifier().getColumnSpan() ) {
throw new AnnotationException(
"An association that targets entity '" + referencedEntity.getEntityName()
+ "' from entity '" + associatedClass.getEntityName()
+ "' has " + columns.length + " '@JoinColumn's but the primary key has "
+ referencedEntity.getIdentifier().getColumnSpan() + " columns"
);
}
linkJoinColumnWithValueOverridingNameIfImplicit(
referencedEntity,
referencedEntity.getIdentifier(),
columns,
value
);
if ( value instanceof SortableValue ) {
( (SortableValue) value).sortProperties();
}
}
private static void bindPkReference(
PersistentClass referencedEntity,
AnnotatedJoinColumn[] columns,
SimpleValue value,
PersistentClass associatedClass,
MetadataBuildingContext buildingContext) {
// ensure the composite key is sorted so that we can simply
// set sorted to true on the ToOne (below)
final KeyValue key = referencedEntity.getKey();
if ( key instanceof Component ) {
( (Component) key).sortProperties();
}
// works because the pk has to be on the primary table
final Dialect dialect = buildingContext.getMetadataCollector().getDatabase()
.getJdbcEnvironment().getDialect();
for ( Column col: key.getColumns() ) {
boolean match = false;
// for each PK column, find the associated FK column.
final String colName = col.getQuotedName( dialect );
for ( AnnotatedJoinColumn joinCol : columns ) {
final String referencedColumn = buildingContext.getMetadataCollector()
.getPhysicalColumnName( referencedEntity.getTable(), joinCol.getReferencedColumn() );
// in JPA 2 referencedColumnName is case-insensitive
if ( referencedColumn.equalsIgnoreCase( colName ) ) {
// correct join column
if ( joinCol.isNameDeferred() ) {
joinCol.linkValueUsingDefaultColumnNaming( col, referencedEntity, value );
}
else {
joinCol.linkWithValue( value );
}
joinCol.overrideFromReferencedColumnIfNecessary( col );
match = true;
break;
}
}
if ( !match ) {
// we can only get here if there's a dupe PK column in the @JoinColumns
throw new AnnotationException(
"An association that targets entity '" + referencedEntity.getEntityName()
+ "' from entity '" + associatedClass.getEntityName()
+ "' has no '@JoinColumn' referencing column '"+ col.getName()
);
}
}
if ( value instanceof ToOne ) {
( (ToOne) value).setSorted( true );
}
}
private static void bindNonPkReference(
PersistentClass referencedEntity,
AnnotatedJoinColumn[] columns,
SimpleValue value) {
final String referencedPropertyName;
if ( value instanceof ToOne ) {
referencedPropertyName = ( (ToOne) value).getReferencedPropertyName();
}
else if ( value instanceof DependantValue ) {
final String propertyName = columns[0].getPropertyName();
if ( propertyName != null ) {
Collection collection = (Collection) referencedEntity.getRecursiveProperty( propertyName ).getValue();
referencedPropertyName = collection.getReferencedPropertyName();
}
else {
throw new AnnotationException( "The '@JoinColumn' for a secondary table must reference the primary key" );
}
}
else {
throw new AssertionFailure( "Property ref to an unexpected Value type: " + value.getClass().getName() );
}
if ( referencedPropertyName == null ) {
throw new AssertionFailure( "No property ref found" );
}
final Property synthProp = referencedEntity.getReferencedProperty( referencedPropertyName );
if ( synthProp == null ) {
throw new AssertionFailure( "Cannot find synthetic property: "
+ referencedEntity.getEntityName() + "." + referencedPropertyName );
}
linkJoinColumnWithValueOverridingNameIfImplicit( referencedEntity, synthProp.getValue(), columns, value );
( (SortableValue) value).sortProperties();
}
private static void bindImplicitColumns(
PersistentClass referencedEntity,
AnnotatedJoinColumn[] columns,
SimpleValue value) {
final List<Column> idColumns = referencedEntity instanceof JoinedSubclass
? referencedEntity.getKey().getColumns()
: referencedEntity.getIdentifier().getColumns();
for ( Column column: idColumns ) {
columns[0].linkValueUsingDefaultColumnNaming( column, referencedEntity, value);
columns[0].overrideFromReferencedColumnIfNecessary( column );
}
}
private static void bindUnownedAssociation(
AnnotatedJoinColumn[] columns,
SimpleValue value,
PersistentClass associatedClass,
String mappedByProperty) {
for ( Column column: mappedByColumns( associatedClass, mappedByProperty ) ) {
columns[0].overrideFromReferencedColumnIfNecessary( column );
columns[0].linkValueUsingAColumnCopy( column, value);
}
}
private static List<Column> mappedByColumns(PersistentClass associatedClass, String mappedByProperty) {
LOG.debugf( "Retrieving property %s.%s", associatedClass.getEntityName(), mappedByProperty );
final Value value = associatedClass.getRecursiveProperty( mappedByProperty ).getValue();
if ( value instanceof Collection ) {
final Value element = ((Collection) value).getElement();
if ( element == null ) {
throw new AnnotationException( "Both sides of the bidirectional association '"
+ associatedClass.getEntityName() + "." + mappedByProperty + "' specify 'mappedBy'" );
}
return element.getColumns();
}
else {
return value.getColumns();
}
}
public static void linkJoinColumnWithValueOverridingNameIfImplicit(
PersistentClass referencedEntity,
Value value,
AnnotatedJoinColumn[] columns,
SimpleValue simpleValue) {
List<Column> valueColumns = value.getColumns();
final List<Column> valueColumns = value.getColumns();
for ( int i = 0; i < columns.length; i++ ) {
AnnotatedJoinColumn joinCol = columns[i];
Column synthCol = valueColumns.get(i);
final AnnotatedJoinColumn joinCol = columns[i];
final Column synthCol = valueColumns.get(i);
if ( joinCol.isNameDeferred() ) {
//this has to be the default value
joinCol.linkValueUsingDefaultColumnNaming( synthCol, referencedEntity, simpleValue );

View File

@ -10,7 +10,6 @@ import java.io.Serializable;
import java.util.List;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;