HHH-12930 fix limitations mapping associations to non-primary unique keys

This now handles cases where the unique key includes @Embeddable properties
of the target entity. It also produces *much* better error messages when
something is wrong.
This commit is contained in:
Gavin King 2022-10-26 22:23:08 +02:00
parent a78a609ecf
commit 3fd84f14ec
22 changed files with 816 additions and 192 deletions

View File

@ -76,6 +76,12 @@ import jakarta.persistence.TableGenerator;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import static org.hibernate.cfg.AnnotatedColumn.buildColumnOrFormulaFromAnnotation; import static org.hibernate.cfg.AnnotatedColumn.buildColumnOrFormulaFromAnnotation;
import static org.hibernate.cfg.AnnotatedJoinColumn.NON_PK_REFERENCE;
import static org.hibernate.cfg.AnnotatedJoinColumn.checkReferencedColumnsType;
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 * @author Emmanuel Bernard
@ -102,7 +108,7 @@ public class BinderHelper {
* create a property copy reusing the same value * create a property copy reusing the same value
*/ */
public static Property shallowCopy(Property property) { public static Property shallowCopy(Property property) {
Property clone = new Property(); Property clone = new SyntheticProperty();
clone.setCascade( property.getCascade() ); clone.setCascade( property.getCascade() );
clone.setInsertable( property.isInsertable() ); clone.setInsertable( property.isInsertable() );
clone.setLazy( property.isLazy() ); clone.setLazy( property.isLazy() );
@ -118,115 +124,156 @@ public class BinderHelper {
return clone; return clone;
} }
/**
* Here we address a fundamental problem: the {@code @JoinColumn}
* annotation specifies the referenced column in the target table
* via {@code referencedColumnName}, but Hibernate needs to know
* which property or field of the target entity class holds the
* value of the referenced column at the Java level. (It's going
* to need the value when it writes the association.)
* <p>
* Complicating this hugely is the fact that an association might
* be based on a composite key with multiple {@code @JoinColumns},
* and so the referenced columns might even be spread out over
* multiple fields or properties of the target entity. There's
* even some extra minor complications resulting from multi-table
* inheritance and secondary tables.
* <p>
* The solution here is:
* <ul>
* <li>if the referenced columns correspond to exactly one property
* of the target entity, we're good, just use it, or
* <li>otherwise, if a composite key is spread out over multiple
* properties, then create a "synthetic" {@link Component} in
* the model that aggregates these properties and is considered
* the target of the association.
* </ul>
* Certain limitations arise from the way this solution is currently
* implemented: for example, if a referenced column belongs to a
* property of an {@code @Embeddable}, then every column of that
* embeddable must occur in the list of referenced columns, and the
* order of the columns must line up! Some of these limitations
* could be relaxed using by writing a better algorithm for building
* the synthetic {@link Component}.
*/
public static void createSyntheticPropertyReference( public static void createSyntheticPropertyReference(
AnnotatedJoinColumn[] columns, AnnotatedJoinColumn[] columns,
PersistentClass ownerEntity, PersistentClass ownerEntity,
//associated entity only used for more precise exception
PersistentClass associatedEntity, PersistentClass associatedEntity,
Value value, Value value,
boolean inverse, boolean inverse,
MetadataBuildingContext context) { MetadataBuildingContext context) {
//associated entity only used for more precise exception, yuk!
if ( columns[0].isImplicit() || StringHelper.isNotEmpty( columns[0].getMappedBy() ) ) {
return;
}
int fkEnum = AnnotatedJoinColumn.checkReferencedColumnsType( columns, ownerEntity, context );
PersistentClass associatedClass = columns[0].getPropertyHolder() != null ?
columns[0].getPropertyHolder().getPersistentClass() :
null;
if ( AnnotatedJoinColumn.NON_PK_REFERENCE == fkEnum ) {
/*
* Create a synthetic property to refer to including an
* embedded component value containing all the properties
* mapped to the referenced columns
* We need to shallow copy those properties to mark them
* as non insertable / non updatable
*/
String syntheticPropertyName =
"_" + associatedClass.getEntityName().replace('.', '_') +
"_" + columns[0].getPropertyName().replace('.', '_');
if ( inverse ) {
// Use a different name for inverse synthetic properties to avoid duplicate properties for self-referencing models
syntheticPropertyName += "_inverse";
}
//find properties associated to a certain column
Object columnOwner = findColumnOwner( ownerEntity, columns[0].getReferencedColumn(), context );
List<Property> properties = findPropertiesByColumns( columnOwner, columns, context );
//create an embeddable component
Property synthProp;
if ( properties != null ) {
//todo how about properties.size() == 1, this should be much simpler
Component embeddedComp = columnOwner instanceof PersistentClass ?
new Component( context, (PersistentClass) columnOwner ) :
new Component( context, (Join) columnOwner );
embeddedComp.setEmbedded( true );
embeddedComp.setComponentClassName( embeddedComp.getOwner().getClassName() );
for (Property property : properties) {
Property clone = shallowCopy( property );
clone.setInsertable( false );
clone.setUpdateable( false );
clone.setNaturalIdentifier( false );
clone.setValueGenerationStrategy( property.getValueGenerationStrategy() );
embeddedComp.addProperty( clone );
}
embeddedComp.sortProperties();
synthProp = new SyntheticProperty();
synthProp.setName( syntheticPropertyName );
synthProp.setPersistentClass( ownerEntity );
synthProp.setUpdateable( false );
synthProp.setInsertable( false );
synthProp.setValue( embeddedComp );
synthProp.setPropertyAccessorName( "embedded" );
ownerEntity.addProperty( synthProp );
//make it unique
embeddedComp.createUniqueKey();
}
else {
//TODO use a ToOne type doing a second select
StringBuilder columnsList = new StringBuilder();
columnsList.append( "referencedColumnNames(" );
for (AnnotatedJoinColumn column : columns) {
columnsList.append( column.getReferencedColumn() ).append( ", " );
}
columnsList.setLength( columnsList.length() - 2 );
columnsList.append( ") " );
if ( associatedEntity != null ) { // TODO: instead of pulling info like the property name and whether
//overridden destination // it's on the owning side off the zeroth column coming in, we
columnsList.append( "of " ) // should receive it directly in the argument list, or from a
.append( associatedEntity.getEntityName() ) // Property instance
.append( "." ) final AnnotatedJoinColumn firstColumn = columns[0];
.append( columns[0].getPropertyName() ) if ( !firstColumn.isImplicit()
.append( " " ); // only necessary for owning side of association
&& isEmpty( firstColumn.getMappedBy() )
// not necessary for a primary key reference
&& checkReferencedColumnsType( columns, ownerEntity, context ) == NON_PK_REFERENCE ) {
// all the columns have to belong to the same table;
// figure out which table has the columns by looking
// for a PersistentClass or Join in the hierarchy of
// the target entity which has the first column
final Object columnOwner = findColumnOwner( ownerEntity, firstColumn.getReferencedColumn(), context );
for ( AnnotatedJoinColumn col: columns ) {
Object owner = findColumnOwner( ownerEntity, col.getReferencedColumn(), context );
if ( owner == null ) {
throw new AnnotationException("A '@JoinColumn' for association "
+ associationMessage( associatedEntity, columns[0] )
+ " references a column named '" + col.getReferencedColumn()
+ "' which is not mapped by the target entity" );
} }
else { if ( owner != columnOwner ) {
if ( columns[0].getPropertyHolder() != null ) { throw new AnnotationException( "The '@JoinColumn's for association "
columnsList.append( "of " ) + associationMessage( associatedEntity, columns[0] )
.append( columns[0].getPropertyHolder().getEntityName() ) + " reference columns of different tables mapped by the target entity ('"
.append( "." ) + col.getReferencedColumn() + "' belongs to a different table to '"
.append( columns[0].getPropertyName() ) + firstColumn.getReferencedColumn() + "'" );
.append( " " );
} }
} }
columnsList.append( "referencing " ) // find all properties mapped to each column
.append( ownerEntity.getEntityName() ) final List<Property> properties = findPropertiesByColumns( columnOwner, columns, associatedEntity, context );
.append( " not mapped to a single property" ); // create a Property along with the new synthetic
throw new AnnotationException( columnsList.toString() ); // Component if necessary (or reuse the existing
// Property that matches exactly)
final Property property = referencedProperty( ownerEntity, inverse, columns, columnOwner, properties, context );
// register the mapping with the InFlightMetadataCollector
registerSyntheticProperty(
ownerEntity,
value,
inverse,
firstColumn.getPropertyHolder().getPersistentClass(),
firstColumn.getPropertyName(),
property.getName(),
context
);
}
} }
/* /**
* creating the property ref to the new synthetic property * If the referenced columns correspond to exactly one property
* of the primary table of the exact target entity subclass,
* just use that property. Otherwise, if a composite key is
* spread out over multiple properties, then create a "synthetic"
* {@link Component} that aggregates these properties and is
* considered the target of the association. This method adds
* the property holding the synthetic component to the target
* entity {@link PersistentClass} by side effect.
*/ */
private static Property referencedProperty(
PersistentClass ownerEntity,
boolean inverse,
AnnotatedJoinColumn[] columns,
Object columnOwner,
List<Property> properties,
MetadataBuildingContext context) {
if ( properties.size() == 1
// necessary to handle the case where the columnOwner is a supertype
&& ownerEntity == columnOwner
//TODO: this is only necessary because of a NotYetImplementedFor6Exception
// in MappingMetamodelCreationHelper.interpretToOneKeyDescriptor
// and ideally we should remove this last condition once that is fixed
&& !( properties.get(0).getValue() instanceof ToOne ) ) {
// no need to make a synthetic property
return properties.get(0);
}
else {
// Create a synthetic Property whose Value is a synthetic
// embeddable component containing the target properties
// mapped to the referenced columns. We need to shallow
// clone those properties to mark them as non-insertable
// and non-updatable
final AnnotatedJoinColumn firstColumn = columns[0];
final PersistentClass associatedClass = firstColumn.getPropertyHolder().getPersistentClass();
final String syntheticPropertyName = syntheticPropertyName( firstColumn.getPropertyName(), inverse, associatedClass );
return makeSyntheticComponentProperty( ownerEntity, columnOwner, context, syntheticPropertyName, properties );
}
}
private static void registerSyntheticProperty(
PersistentClass ownerEntity,
Value value,
boolean inverse,
PersistentClass associatedClass,
String propertyName,
String syntheticPropertyName,
MetadataBuildingContext context) {
if ( value instanceof ToOne ) { if ( value instanceof ToOne ) {
( (ToOne) value ).setReferencedPropertyName( syntheticPropertyName ); ( (ToOne) value).setReferencedPropertyName( syntheticPropertyName );
( (ToOne) value ).setReferenceToPrimaryKey( false ); ( (ToOne) value).setReferenceToPrimaryKey( false );
context.getMetadataCollector().addUniquePropertyReference( context.getMetadataCollector().addUniquePropertyReference(
ownerEntity.getEntityName(), ownerEntity.getEntityName(),
syntheticPropertyName syntheticPropertyName
); );
} }
else if ( value instanceof Collection ) { else if ( value instanceof Collection ) {
( (Collection) value ).setReferencedPropertyName( syntheticPropertyName ); ( (Collection) value).setReferencedPropertyName( syntheticPropertyName );
//not unique because we could create a mtm wo association table //not unique because we could create a mtm wo association table
context.getMetadataCollector().addPropertyReference( context.getMetadataCollector().addPropertyReference(
ownerEntity.getEntityName(), ownerEntity.getEntityName(),
@ -241,20 +288,111 @@ public class BinderHelper {
} }
context.getMetadataCollector().addPropertyReferencedAssociation( context.getMetadataCollector().addPropertyReferencedAssociation(
( inverse ? "inverse__" : "" ) + associatedClass.getEntityName(), ( inverse ? "inverse__" : "" ) + associatedClass.getEntityName(),
columns[0].getPropertyName(), propertyName,
syntheticPropertyName syntheticPropertyName
); );
} }
private static String syntheticPropertyName(
String propertyName,
boolean inverse,
PersistentClass associatedClass) {
String syntheticPropertyName =
"_" + associatedClass.getEntityName().replace('.', '_') +
"_" + propertyName.replace('.', '_');
if (inverse) {
// Use a different name for inverse synthetic properties to avoid duplicate properties for self-referencing models
syntheticPropertyName += "_inverse";
}
return syntheticPropertyName;
} }
private static String associationMessage(PersistentClass associatedEntity, AnnotatedJoinColumn firstColumn) {
StringBuilder message = new StringBuilder();
if ( associatedEntity != null ) {
message.append( "'" )
.append( associatedEntity.getEntityName() )
.append( "." )
.append( firstColumn.getPropertyName() )
.append( "'" );
}
else {
if ( firstColumn.getPropertyHolder() != null ) {
message.append( "'" )
.append( firstColumn.getPropertyHolder().getEntityName() )
.append( "." )
.append( firstColumn.getPropertyName() )
.append( "'" );
}
}
return message.toString();
}
private static Property makeSyntheticComponentProperty(
PersistentClass ownerEntity,
Object persistentClassOrJoin,
MetadataBuildingContext context,
String syntheticPropertyName,
List<Property> properties) {
Component embeddedComp = persistentClassOrJoin instanceof PersistentClass
? new Component( context, (PersistentClass) persistentClassOrJoin )
: new Component( context, (Join) persistentClassOrJoin );
embeddedComp.setComponentClassName( embeddedComp.getOwner().getClassName() );
embeddedComp.setEmbedded( true );
Property property = makeComponent( ownerEntity, context, syntheticPropertyName, embeddedComp, properties );
property.setPropertyAccessorName( "embedded" );
ownerEntity.addProperty( property );
embeddedComp.createUniqueKey(); //make it unique
return property;
}
private static Property makeComponent(
PersistentClass ownerEntity,
MetadataBuildingContext context,
String name,
Component embeddedComp,
List<Property> properties) {
for ( Property property : properties ) {
Property clone = cloneProperty( ownerEntity, context, property );
embeddedComp.addProperty( clone );
}
embeddedComp.sortProperties();
Property synthProp = new SyntheticProperty();
synthProp.setName( name );
synthProp.setPersistentClass( ownerEntity );
synthProp.setUpdateable( false );
synthProp.setInsertable( false );
synthProp.setValue( embeddedComp );
return synthProp;
}
private static Property cloneProperty(PersistentClass ownerEntity, MetadataBuildingContext context, Property property) {
if ( property.isComposite() ) {
Component component = (Component) property.getValue();
Component copy = new Component( context, component );
copy.setComponentClassName( component.getComponentClassName() );
copy.setEmbedded( component.isEmbedded() );
Property clone = makeComponent( ownerEntity, context, property.getName(), copy, component.getProperties() );
clone.setPropertyAccessorName( property.getPropertyAccessorName() );
return clone;
}
else {
Property clone = shallowCopy( property );
clone.setInsertable( false );
clone.setUpdateable( false );
clone.setNaturalIdentifier( false );
clone.setValueGenerationStrategy( property.getValueGenerationStrategy() );
return clone;
}
}
private static List<Property> findPropertiesByColumns( private static List<Property> findPropertiesByColumns(
Object columnOwner, Object columnOwner,
AnnotatedJoinColumn[] columns, AnnotatedJoinColumn[] columns,
PersistentClass associatedEntity,
MetadataBuildingContext context) { MetadataBuildingContext context) {
Map<Column, Set<Property>> columnsToProperty = new HashMap<>();
List<Column> orderedColumns = new ArrayList<>( columns.length ); final Table referencedTable;
Table referencedTable;
if ( columnOwner instanceof PersistentClass ) { if ( columnOwner instanceof PersistentClass ) {
referencedTable = ( (PersistentClass) columnOwner ).getTable(); referencedTable = ( (PersistentClass) columnOwner ).getTable();
} }
@ -268,66 +406,126 @@ public class BinderHelper {
"columnOwner neither PersistentClass nor Join: " + columnOwner.getClass() "columnOwner neither PersistentClass nor Join: " + columnOwner.getClass()
); );
} }
//build the list of column names
for (AnnotatedJoinColumn column1 : columns) { // Build the list of column names in the exact order they were
// specified by the @JoinColumn annotations.
final List<Column> orderedColumns = new ArrayList<>( columns.length );
final Map<Column, Set<Property>> columnsToProperty = new HashMap<>();
for ( AnnotatedJoinColumn joinColumn : columns ) {
Column column = new Column( Column column = new Column(
context.getMetadataCollector().getPhysicalColumnName( context.getMetadataCollector().getPhysicalColumnName(
referencedTable, referencedTable,
column1.getReferencedColumn() joinColumn.getReferencedColumn()
) )
); );
orderedColumns.add( column ); orderedColumns.add( column );
columnsToProperty.put( column, new HashSet<>() ); columnsToProperty.put( column, new HashSet<>() );
} }
boolean isPersistentClass = columnOwner instanceof PersistentClass;
List<Property> properties = isPersistentClass ? // Now, for each column find the properties of the target entity
( (PersistentClass) columnOwner ).getProperties() : // which are mapped to that column. (There might be multiple such
( (Join) columnOwner ).getProperties(); // properties for each column.)
for (Property property : properties) { if ( columnOwner instanceof PersistentClass ) {
PersistentClass persistentClass = (PersistentClass) columnOwner;
for ( Property property : persistentClass.getProperties() ) {
matchColumnsByProperty( property, columnsToProperty );
}
matchColumnsByProperty( persistentClass.getIdentifierProperty(), columnsToProperty );
}
else {
for ( Property property : ((Join) columnOwner).getProperties() ) {
matchColumnsByProperty( property, columnsToProperty ); matchColumnsByProperty( property, columnsToProperty );
} }
if ( isPersistentClass ) {
matchColumnsByProperty( ( (PersistentClass) columnOwner ).getIdentifierProperty(), columnsToProperty );
} }
//first naive implementation // Now we need to line up the properties with the columns in the
//only check 1 columns properties // same order they were specified by the @JoinColumn annotations
//TODO make it smarter by checking correctly ordered multi column properties // this is very tricky because a single property might span
// multiple columns.
// TODO: For now we only consider the first property that matched
// each column, but this means we will reject some mappings
// that could be made to work for a different choice of
// properties (it's also not very deterministic)
List<Property> orderedProperties = new ArrayList<>(); List<Property> orderedProperties = new ArrayList<>();
for (Column column : orderedColumns) { int lastPropertyColumnIndex = 0;
boolean found = false; Property currentProperty = null;
for (Property property : columnsToProperty.get( column ) ) { for ( Column column : orderedColumns ) {
if ( property.getColumnSpan() == 1 ) { Set<Property> properties = columnsToProperty.get( column );
if ( properties.isEmpty() ) {
// no property found which maps to this column
throw new AnnotationException( "Referenced column '" + column.getName()
+ "' in '@JoinColumn' for " + associationMessage( associatedEntity, columns[0] )
+ " is not mapped by any property of the target entity" );
}
for ( Property property : properties ) {
if ( property == currentProperty ) {
// we have the next column of the previous property
if ( !property.getColumns().get( lastPropertyColumnIndex ).equals( column ) ) {
// the columns have to occur in the right order in the property
throw new AnnotationException( "Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '@JoinColumn's for association "
+ associationMessage( associatedEntity, columns[0] ) );
}
lastPropertyColumnIndex++;
if ( lastPropertyColumnIndex == currentProperty.getColumnSpan() ) {
//we have exhausted the columns in this property
currentProperty = null;
lastPropertyColumnIndex = 0;
}
}
else if ( currentProperty != null ) {
// we didn't use up all the columns of the previous property
throw new AnnotationException( "Target property '" + property.getName() + "' has "
+ property.getColumnSpan() + " columns which must be referenced by a '@JoinColumn' for "
+ associationMessage( associatedEntity, columns[0] )
+ " (every column mapped by '" + property.getName()
+ "' must occur exactly once as a 'referencedColumnName', and in the correct order)" );
}
else if ( orderedProperties.contains( property ) ) {
// we already used up all the columns of this property
throw new AnnotationException( "Target property '" + property.getName() + "' has only "
+ property.getColumnSpan() + " columns which may be referenced by a '@JoinColumn' for "
+ associationMessage( associatedEntity, columns[0] )
+ " (each column mapped by '" + property.getName()
+ "' may only occur once as a 'referencedColumnName')" );
}
else {
// we have the first column of a new property
orderedProperties.add( property ); orderedProperties.add( property );
found = true; if ( property.getColumnSpan() > 1 ) {
break; if ( !property.getColumns().get(0).equals( column ) ) {
// the columns have to occur in the right order in the property
throw new AnnotationException("Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '@JoinColumn's");
}
currentProperty = property;
lastPropertyColumnIndex = 1;
} }
} }
if ( !found ) { break; // we're only considering the first matching property for now
//have to find it the hard way
return null;
} }
} }
return orderedProperties; return orderedProperties;
} }
private static void matchColumnsByProperty(Property property, Map<Column, Set<Property>> columnsToProperty) { private static void matchColumnsByProperty(Property property, Map<Column, Set<Property>> columnsToProperty) {
if ( property == null ) { if ( property != null
return; && NOOP != interpret( property.getPropertyAccessorName() )
} && EMBEDDED != interpret( property.getPropertyAccessorName() ) ) {
if ( "noop".equals( property.getPropertyAccessorName() ) //TODO: we can't return subproperties because the caller
|| "embedded".equals( property.getPropertyAccessorName() ) ) { // needs top level properties, but this results in
return; // a limitation where I need to be referencing all
} // columns of an embeddable instead of just some
// FIXME cannot use subproperties because the caller needs top level properties
// if ( property.isComposite() ) { // if ( property.isComposite() ) {
// Iterator subProperties = ( (Component) property.getValue() ).getPropertyIterator(); // for ( Property sp : ( (Component) property.getValue() ).getProperties() ) {
// while ( subProperties.hasNext() ) { // matchColumnsByProperty( sp, columnsToProperty );
// matchColumnsByProperty( (Property) subProperties.next(), columnsToProperty );
// } // }
// } // }
else { // else {
for (Selectable selectable : property.getSelectables()) { for ( Selectable selectable : property.getSelectables() ) {
//can be a Formula, so we don't cast //can be a Formula, so we don't cast
//noinspection SuspiciousMethodCalls //noinspection SuspiciousMethodCalls
if ( columnsToProperty.containsKey( selectable ) ) { if ( columnsToProperty.containsKey( selectable ) ) {
@ -335,6 +533,7 @@ public class BinderHelper {
columnsToProperty.get( selectable ).add( property ); columnsToProperty.get( selectable ).add( property );
} }
} }
// }
} }
} }
@ -476,30 +675,23 @@ public class BinderHelper {
PersistentClass persistentClass, PersistentClass persistentClass,
String columnName, String columnName,
MetadataBuildingContext context) { MetadataBuildingContext context) {
if ( StringHelper.isEmpty( columnName ) ) { if ( isEmpty( columnName ) ) {
//shortcut for implicit referenced column names //shortcut for implicit referenced column names
return persistentClass; return persistentClass;
} }
PersistentClass current = persistentClass; PersistentClass current = persistentClass;
Object result; while ( current != null ) {
boolean found = false;
do {
result = current;
Table currentTable = current.getTable();
try { try {
context.getMetadataCollector().getPhysicalColumnName( currentTable, columnName ); context.getMetadataCollector().getPhysicalColumnName( current.getTable(), columnName );
found = true; return current;
} }
catch (MappingException me) { catch (MappingException me) {
//swallow it //swallow it
} }
Iterator<Join> joins = current.getJoinIterator(); for ( Join join : current.getJoins() ) {
while ( !found && joins.hasNext() ) {
result = joins.next();
currentTable = ( (Join) result ).getTable();
try { try {
context.getMetadataCollector().getPhysicalColumnName( currentTable, columnName ); context.getMetadataCollector().getPhysicalColumnName( join.getTable(), columnName );
found = true; return join;
} }
catch (MappingException me) { catch (MappingException me) {
//swallow it //swallow it
@ -507,8 +699,7 @@ public class BinderHelper {
} }
current = current.getSuperclass(); current = current.getSuperclass();
} }
while ( !found && current != null ); return null;
return found ? result : null;
} }
/** /**
@ -531,15 +722,10 @@ public class BinderHelper {
Properties params = new Properties(); Properties params = new Properties();
//always settable //always settable
params.setProperty( params.setProperty( PersistentIdentifierGenerator.TABLE, table.getName() );
PersistentIdentifierGenerator.TABLE, table.getName()
);
if ( id.getColumnSpan() == 1 ) { if ( id.getColumnSpan() == 1 ) {
params.setProperty( params.setProperty( PersistentIdentifierGenerator.PK, id.getColumns().get(0).getName() );
PersistentIdentifierGenerator.PK,
id.getColumns().get(0).getName()
);
} }
// YUCK! but cannot think of a clean way to do this given the string-config based scheme // YUCK! but cannot think of a clean way to do this given the string-config based scheme
params.put( PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, buildingContext.getObjectNameNormalizer() ); params.put( PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, buildingContext.getObjectNameNormalizer() );

View File

@ -394,18 +394,28 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
} }
private Type getTypeForEntityValue(Mapping mapping, Type type, int typeIndex) { private Type getTypeForEntityValue(Mapping mapping, Type type, int typeIndex) {
while ( !( type instanceof JdbcMapping ) ) { int index = 0;
//ManyToOneType doesn't implement JdbcMapping if ( type instanceof EntityType ) {
type = ( (EntityType) type ).getIdentifierOrUniqueKeyType( mapping ); EntityType entityType = (EntityType) type;
if ( type instanceof ComponentType ) { return getTypeForEntityValue( mapping, entityType.getIdentifierOrUniqueKeyType( mapping ), typeIndex );
type = ( (ComponentType) type ).getSubtypes()[typeIndex];
while (type instanceof ComponentType) {
type = ( (ComponentType) type ).getSubtypes()[0];
} }
else if ( type instanceof ComponentType ) {
for (Type subtype : ((ComponentType) type).getSubtypes() ) {
Type result = getTypeForEntityValue( mapping, subtype, typeIndex - index );
if ( result != null ) {
return result;
} }
index += subtype.getColumnSpan( mapping );
} }
return null;
}
else if ( typeIndex == 0 ) {
return type; return type;
} }
else {
return null;
}
}
public String getSqlType() { public String getSqlType() {
return sqlType; return sqlType;

View File

@ -0,0 +1,36 @@
package org.hibernate.orm.test.annotations.refcolnames.basics;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@DomainModel(annotatedClasses = {Region.class, Town.class})
@SessionFactory
public class BasicsRefColNamesTest {
@Test
public void test(SessionFactoryScope scope) {
Region region = new Region();
PostalCode postalCode = new PostalCode();
postalCode.countryCode = "ES";
postalCode.zipCode = 69;
region.countryCode = postalCode.countryCode;
region.zipCode = postalCode.zipCode;
Town town = new Town();
TownCode townCode = new TownCode();
townCode.town = "Barcelona";
townCode.countryCode = "ES";
townCode.zipCode = 69;
town.region = region;
town.townCode = townCode;
scope.inTransaction(s -> {
s.persist(region);
s.persist(town);
});
scope.inTransaction(s -> {
Town t = s.createQuery("from Town join fetch region", Town.class).getSingleResult();
Assertions.assertNotNull(t);
});
}
}

View File

@ -0,0 +1,13 @@
package org.hibernate.orm.test.annotations.refcolnames.basics;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.MappedSuperclass;
@Embeddable @MappedSuperclass
class PostalCode {
@Column(name="country_code", nullable = false)
String countryCode;
@Column(name="zip_code", nullable = false)
int zipCode;
}

View File

@ -0,0 +1,28 @@
package org.hibernate.orm.test.annotations.refcolnames.basics;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
@Entity
class Region {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
int id;
String name;
@NaturalId
@Column(name = "country_code", nullable = false)
String countryCode;
@NaturalId
@Column(name = "zip_code", nullable = false)
int zipCode;
}

View File

@ -0,0 +1,31 @@
package org.hibernate.orm.test.annotations.refcolnames.basics;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
@Entity
class Town {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
Integer id;
String name;
@NaturalId
@Embedded
TownCode townCode;
@ManyToOne
@JoinColumn(name = "country_code", referencedColumnName = "country_code", nullable = false, insertable = false, updatable = false)
@JoinColumn(name = "zip_code", referencedColumnName = "zip_code", nullable = false, insertable = false, updatable = false)
Region region;
}

View File

@ -0,0 +1,8 @@
package org.hibernate.orm.test.annotations.refcolnames.basics;
import jakarta.persistence.Embeddable;
@Embeddable
class TownCode extends PostalCode {
String town;
}

View File

@ -0,0 +1,35 @@
package org.hibernate.orm.test.annotations.refcolnames.embedded;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@DomainModel(annotatedClasses = {Region.class, Town.class})
@SessionFactory
public class EmbeddedRefColNamesTest {
@Test
public void test(SessionFactoryScope scope) {
Region region = new Region();
PostalCode postalCode = new PostalCode();
postalCode.countryCode = "ES";
postalCode.zipCode = 69;
region.postalCode = postalCode;
Town town = new Town();
TownCode townCode = new TownCode();
townCode.town = "Barcelona";
townCode.countryCode = "ES";
townCode.zipCode = 69;
town.region = region;
town.townCode = townCode;
scope.inTransaction(s -> {
s.persist(region);
s.persist(town);
});
scope.inTransaction(s -> {
Town t = s.createQuery("from Town join fetch region", Town.class).getSingleResult();
Assertions.assertNotNull(t);
});
}
}

View File

@ -0,0 +1,13 @@
package org.hibernate.orm.test.annotations.refcolnames.embedded;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.MappedSuperclass;
@Embeddable @MappedSuperclass
class PostalCode {
@Column(name="country_code", nullable = false)
String countryCode;
@Column(name="zip_code", nullable = false)
int zipCode;
}

View File

@ -0,0 +1,25 @@
package org.hibernate.orm.test.annotations.refcolnames.embedded;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
@Entity
class Region {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
int id;
String name;
@NaturalId
@Embedded
PostalCode postalCode;
}

View File

@ -0,0 +1,31 @@
package org.hibernate.orm.test.annotations.refcolnames.embedded;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
@Entity
class Town {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
Integer id;
String name;
@NaturalId
@Embedded
TownCode townCode;
@ManyToOne
@JoinColumn(name = "country_code", referencedColumnName = "country_code", nullable = false, insertable = false, updatable = false)
@JoinColumn(name = "zip_code", referencedColumnName = "zip_code", nullable = false, insertable = false, updatable = false)
Region region;
}

View File

@ -0,0 +1,8 @@
package org.hibernate.orm.test.annotations.refcolnames.embedded;
import jakarta.persistence.Embeddable;
@Embeddable
class TownCode extends PostalCode {
String town;
}

View File

@ -0,0 +1,35 @@
package org.hibernate.orm.test.annotations.refcolnames.embeddedid;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@DomainModel(annotatedClasses = {Region.class, Town.class})
@SessionFactory
public class EmbeddedIdRefColNamesTest {
@Test
public void test(SessionFactoryScope scope) {
Region region = new Region();
PostalCode postalCode = new PostalCode();
postalCode.countryCode = "ES";
postalCode.zipCode = 69;
region.postalCode = postalCode;
Town town = new Town();
TownCode townCode = new TownCode();
townCode.town = "Barcelona";
townCode.countryCode = "ES";
townCode.zipCode = 69;
town.region = region;
town.townCode = townCode;
scope.inTransaction(s -> {
s.persist(region);
s.persist(town);
});
scope.inTransaction(s -> {
Town t = s.createQuery("from Town join fetch region", Town.class).getSingleResult();
Assertions.assertNotNull(t);
});
}
}

View File

@ -0,0 +1,13 @@
package org.hibernate.orm.test.annotations.refcolnames.embeddedid;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.MappedSuperclass;
@Embeddable @MappedSuperclass
class PostalCode {
@Column(name="country_code")
String countryCode;
@Column(name="zip_code")
int zipCode;
}

View File

@ -0,0 +1,10 @@
package org.hibernate.orm.test.annotations.refcolnames.embeddedid;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
@Entity
class Region {
@EmbeddedId
PostalCode postalCode;
}

View File

@ -0,0 +1,17 @@
package org.hibernate.orm.test.annotations.refcolnames.embeddedid;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@Entity
class Town {
@EmbeddedId
TownCode townCode;
@ManyToOne
@JoinColumn(name = "zip_code", referencedColumnName = "zip_code", insertable = false, updatable = false)
@JoinColumn(name = "country_code", referencedColumnName = "country_code", insertable = false, updatable = false)
Region region;
}

View File

@ -0,0 +1,8 @@
package org.hibernate.orm.test.annotations.refcolnames.embeddedid;
import jakarta.persistence.Embeddable;
@Embeddable
class TownCode extends PostalCode {
String town;
}

View File

@ -0,0 +1,36 @@
package org.hibernate.orm.test.annotations.refcolnames.mixed;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@DomainModel(annotatedClasses = {Region.class, Town.class})
@SessionFactory
public class MixedRefColNamesTest {
@Test
public void test(SessionFactoryScope scope) {
Region region = new Region();
PostalCode postalCode = new PostalCode();
postalCode.countryCode = "ES";
postalCode.zipCode = 69;
region.postalCode = postalCode;
Town town = new Town();
TownCode townCode = new TownCode();
townCode.town = "Barcelona";
townCode.countryCode = "ES";
townCode.zipCode = 69;
town.region = region;
town.townCode = townCode;
scope.inTransaction(s -> {
s.persist(region);
town.regionId = region.id;
s.persist(town);
});
scope.inTransaction(s -> {
Town t = s.createQuery("from Town join fetch region", Town.class).getSingleResult();
Assertions.assertNotNull(t);
});
}
}

View File

@ -0,0 +1,13 @@
package org.hibernate.orm.test.annotations.refcolnames.mixed;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.MappedSuperclass;
@Embeddable @MappedSuperclass
class PostalCode {
@Column(name="country_code", nullable = false)
String countryCode;
@Column(name="zip_code", nullable = false)
int zipCode;
}

View File

@ -0,0 +1,25 @@
package org.hibernate.orm.test.annotations.refcolnames.mixed;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
@Entity
class Region {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
int id;
String name;
@NaturalId
@Embedded
PostalCode postalCode;
}

View File

@ -0,0 +1,35 @@
package org.hibernate.orm.test.annotations.refcolnames.mixed;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
@Entity
class Town {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
Integer id;
String name;
@NaturalId
@Embedded
TownCode townCode;
@Column(name = "region_id", nullable = false)
int regionId;
@ManyToOne
@JoinColumn(name = "region_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false)
@JoinColumn(name = "country_code", referencedColumnName = "country_code", nullable = false, insertable = false, updatable = false)
@JoinColumn(name = "zip_code", referencedColumnName = "zip_code", nullable = false, insertable = false, updatable = false)
Region region;
}

View File

@ -0,0 +1,8 @@
package org.hibernate.orm.test.annotations.refcolnames.mixed;
import jakarta.persistence.Embeddable;
@Embeddable
class TownCode extends PostalCode {
String town;
}