diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java b/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java index 66db5c887c..c431db0a1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java @@ -76,6 +76,12 @@ import jakarta.persistence.TableGenerator; import jakarta.persistence.UniqueConstraint; 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 @@ -102,7 +108,7 @@ public class BinderHelper { * create a property copy reusing the same value */ public static Property shallowCopy(Property property) { - Property clone = new Property(); + Property clone = new SyntheticProperty(); clone.setCascade( property.getCascade() ); clone.setInsertable( property.isInsertable() ); clone.setLazy( property.isLazy() ); @@ -118,143 +124,275 @@ public class BinderHelper { 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.) + *

+ * 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. + *

+ * The solution here is: + *

+ * 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( AnnotatedJoinColumn[] columns, PersistentClass ownerEntity, + //associated entity only used for more precise exception PersistentClass associatedEntity, Value value, boolean inverse, 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 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 ) { - //overridden destination - columnsList.append( "of " ) - .append( associatedEntity.getEntityName() ) - .append( "." ) - .append( columns[0].getPropertyName() ) - .append( " " ); - } - else { - if ( columns[0].getPropertyHolder() != null ) { - columnsList.append( "of " ) - .append( columns[0].getPropertyHolder().getEntityName() ) - .append( "." ) - .append( columns[0].getPropertyName() ) - .append( " " ); - } - } - columnsList.append( "referencing " ) - .append( ownerEntity.getEntityName() ) - .append( " not mapped to a single property" ); - throw new AnnotationException( columnsList.toString() ); - } + // TODO: instead of pulling info like the property name and whether + // it's on the owning side off the zeroth column coming in, we + // should receive it directly in the argument list, or from a + // Property instance + final AnnotatedJoinColumn firstColumn = columns[0]; + if ( !firstColumn.isImplicit() + // only necessary for owning side of association + && isEmpty( firstColumn.getMappedBy() ) + // not necessary for a primary key reference + && checkReferencedColumnsType( columns, ownerEntity, context ) == NON_PK_REFERENCE ) { - /* - * creating the property ref to the new synthetic property - */ - if ( value instanceof ToOne ) { - ( (ToOne) value ).setReferencedPropertyName( syntheticPropertyName ); - ( (ToOne) value ).setReferenceToPrimaryKey( false ); - context.getMetadataCollector().addUniquePropertyReference( - ownerEntity.getEntityName(), - syntheticPropertyName - ); + // 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" ); + } + if ( owner != columnOwner ) { + throw new AnnotationException( "The '@JoinColumn's for association " + + associationMessage( associatedEntity, columns[0] ) + + " reference columns of different tables mapped by the target entity ('" + + col.getReferencedColumn() + "' belongs to a different table to '" + + firstColumn.getReferencedColumn() + "'" ); + } } - else if ( value instanceof Collection ) { - ( (Collection) value ).setReferencedPropertyName( syntheticPropertyName ); - //not unique because we could create a mtm wo association table - context.getMetadataCollector().addPropertyReference( - ownerEntity.getEntityName(), - syntheticPropertyName - ); - } - else { - throw new AssertionFailure( - "Do a property ref on an unexpected Value type: " - + value.getClass().getName() - ); - } - context.getMetadataCollector().addPropertyReferencedAssociation( - ( inverse ? "inverse__" : "" ) + associatedClass.getEntityName(), - columns[0].getPropertyName(), - syntheticPropertyName + // find all properties mapped to each column + final List properties = findPropertiesByColumns( columnOwner, columns, associatedEntity, context ); + // create a Property along with the new synthetic + // 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 ); } } + /** + * 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 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 ) { + ( (ToOne) value).setReferencedPropertyName( syntheticPropertyName ); + ( (ToOne) value).setReferenceToPrimaryKey( false ); + context.getMetadataCollector().addUniquePropertyReference( + ownerEntity.getEntityName(), + syntheticPropertyName + ); + } + else if ( value instanceof Collection ) { + ( (Collection) value).setReferencedPropertyName( syntheticPropertyName ); + //not unique because we could create a mtm wo association table + context.getMetadataCollector().addPropertyReference( + ownerEntity.getEntityName(), + syntheticPropertyName + ); + } + else { + throw new AssertionFailure( + "Do a property ref on an unexpected Value type: " + + value.getClass().getName() + ); + } + context.getMetadataCollector().addPropertyReferencedAssociation( + ( inverse ? "inverse__" : "" ) + associatedClass.getEntityName(), + propertyName, + 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 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 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 findPropertiesByColumns( Object columnOwner, AnnotatedJoinColumn[] columns, + PersistentClass associatedEntity, MetadataBuildingContext context) { - Map> columnsToProperty = new HashMap<>(); - List orderedColumns = new ArrayList<>( columns.length ); - Table referencedTable; + + final Table referencedTable; if ( columnOwner instanceof PersistentClass ) { referencedTable = ( (PersistentClass) columnOwner ).getTable(); } @@ -268,66 +406,126 @@ public class BinderHelper { "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 orderedColumns = new ArrayList<>( columns.length ); + final Map> columnsToProperty = new HashMap<>(); + for ( AnnotatedJoinColumn joinColumn : columns ) { Column column = new Column( context.getMetadataCollector().getPhysicalColumnName( referencedTable, - column1.getReferencedColumn() + joinColumn.getReferencedColumn() ) ); orderedColumns.add( column ); columnsToProperty.put( column, new HashSet<>() ); } - boolean isPersistentClass = columnOwner instanceof PersistentClass; - List properties = isPersistentClass ? - ( (PersistentClass) columnOwner ).getProperties() : - ( (Join) columnOwner ).getProperties(); - for (Property property : properties) { - matchColumnsByProperty( property, columnsToProperty ); + + // Now, for each column find the properties of the target entity + // which are mapped to that column. (There might be multiple such + // properties for each column.) + if ( columnOwner instanceof PersistentClass ) { + PersistentClass persistentClass = (PersistentClass) columnOwner; + for ( Property property : persistentClass.getProperties() ) { + matchColumnsByProperty( property, columnsToProperty ); + } + matchColumnsByProperty( persistentClass.getIdentifierProperty(), columnsToProperty ); } - if ( isPersistentClass ) { - matchColumnsByProperty( ( (PersistentClass) columnOwner ).getIdentifierProperty(), columnsToProperty ); + else { + for ( Property property : ((Join) columnOwner).getProperties() ) { + matchColumnsByProperty( property, columnsToProperty ); + } } - //first naive implementation - //only check 1 columns properties - //TODO make it smarter by checking correctly ordered multi column properties + // Now we need to line up the properties with the columns in the + // same order they were specified by the @JoinColumn annotations + // 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 orderedProperties = new ArrayList<>(); - for (Column column : orderedColumns) { - boolean found = false; - for (Property property : columnsToProperty.get( column ) ) { - if ( property.getColumnSpan() == 1 ) { - orderedProperties.add( property ); - found = true; - break; - } + int lastPropertyColumnIndex = 0; + Property currentProperty = null; + for ( Column column : orderedColumns ) { + Set 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" ); } - if ( !found ) { - //have to find it the hard way - return null; + 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 ); + if ( property.getColumnSpan() > 1 ) { + 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; + } + } + break; // we're only considering the first matching property for now } } return orderedProperties; } private static void matchColumnsByProperty(Property property, Map> columnsToProperty) { - if ( property == null ) { - return; - } - if ( "noop".equals( property.getPropertyAccessorName() ) - || "embedded".equals( property.getPropertyAccessorName() ) ) { - return; - } -// FIXME cannot use subproperties because the caller needs top level properties -// if ( property.isComposite() ) { -// Iterator subProperties = ( (Component) property.getValue() ).getPropertyIterator(); -// while ( subProperties.hasNext() ) { -// matchColumnsByProperty( (Property) subProperties.next(), columnsToProperty ); + if ( property != null + && NOOP != interpret( property.getPropertyAccessorName() ) + && EMBEDDED != interpret( property.getPropertyAccessorName() ) ) { + //TODO: we can't return subproperties because the caller + // needs top level properties, but this results in + // a limitation where I need to be referencing all + // columns of an embeddable instead of just some +// if ( property.isComposite() ) { +// for ( Property sp : ( (Component) property.getValue() ).getProperties() ) { +// matchColumnsByProperty( sp, columnsToProperty ); +// } // } -// } - else { - for (Selectable selectable : property.getSelectables()) { +// else { + for ( Selectable selectable : property.getSelectables() ) { //can be a Formula, so we don't cast //noinspection SuspiciousMethodCalls if ( columnsToProperty.containsKey( selectable ) ) { @@ -335,6 +533,7 @@ public class BinderHelper { columnsToProperty.get( selectable ).add( property ); } } +// } } } @@ -476,30 +675,23 @@ public class BinderHelper { PersistentClass persistentClass, String columnName, MetadataBuildingContext context) { - if ( StringHelper.isEmpty( columnName ) ) { + if ( isEmpty( columnName ) ) { //shortcut for implicit referenced column names return persistentClass; } PersistentClass current = persistentClass; - Object result; - boolean found = false; - do { - result = current; - Table currentTable = current.getTable(); + while ( current != null ) { try { - context.getMetadataCollector().getPhysicalColumnName( currentTable, columnName ); - found = true; + context.getMetadataCollector().getPhysicalColumnName( current.getTable(), columnName ); + return current; } catch (MappingException me) { //swallow it } - Iterator joins = current.getJoinIterator(); - while ( !found && joins.hasNext() ) { - result = joins.next(); - currentTable = ( (Join) result ).getTable(); + for ( Join join : current.getJoins() ) { try { - context.getMetadataCollector().getPhysicalColumnName( currentTable, columnName ); - found = true; + context.getMetadataCollector().getPhysicalColumnName( join.getTable(), columnName ); + return join; } catch (MappingException me) { //swallow it @@ -507,8 +699,7 @@ public class BinderHelper { } current = current.getSuperclass(); } - while ( !found && current != null ); - return found ? result : null; + return null; } /** @@ -531,15 +722,10 @@ public class BinderHelper { Properties params = new Properties(); //always settable - params.setProperty( - PersistentIdentifierGenerator.TABLE, table.getName() - ); + params.setProperty( PersistentIdentifierGenerator.TABLE, table.getName() ); if ( id.getColumnSpan() == 1 ) { - params.setProperty( - PersistentIdentifierGenerator.PK, - id.getColumns().get(0).getName() - ); + params.setProperty( PersistentIdentifierGenerator.PK, id.getColumns().get(0).getName() ); } // YUCK! but cannot think of a clean way to do this given the string-config based scheme params.put( PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, buildingContext.getObjectNameNormalizer() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index 832ef020f0..f1cb5e0852 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -394,17 +394,27 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn } private Type getTypeForEntityValue(Mapping mapping, Type type, int typeIndex) { - while ( !( type instanceof JdbcMapping ) ) { - //ManyToOneType doesn't implement JdbcMapping - type = ( (EntityType) type ).getIdentifierOrUniqueKeyType( mapping ); - if ( type instanceof ComponentType ) { - type = ( (ComponentType) type ).getSubtypes()[typeIndex]; - while (type instanceof ComponentType) { - type = ( (ComponentType) type ).getSubtypes()[0]; - } - } + int index = 0; + if ( type instanceof EntityType ) { + EntityType entityType = (EntityType) type; + return getTypeForEntityValue( mapping, entityType.getIdentifierOrUniqueKeyType( mapping ), typeIndex ); + } + 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; + } + else { + return null; } - return type; } public String getSqlType() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/BasicsRefColNamesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/BasicsRefColNamesTest.java new file mode 100644 index 0000000000..c3470f4c58 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/BasicsRefColNamesTest.java @@ -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); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/PostalCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/PostalCode.java new file mode 100644 index 0000000000..3df9501fdb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/PostalCode.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Region.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Region.java new file mode 100644 index 0000000000..533ac25475 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Region.java @@ -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; + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Town.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Town.java new file mode 100644 index 0000000000..f0e54ec3fc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/Town.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/TownCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/TownCode.java new file mode 100644 index 0000000000..86f6c3f7ab --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/basics/TownCode.java @@ -0,0 +1,8 @@ +package org.hibernate.orm.test.annotations.refcolnames.basics; + +import jakarta.persistence.Embeddable; + +@Embeddable +class TownCode extends PostalCode { + String town; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/EmbeddedRefColNamesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/EmbeddedRefColNamesTest.java new file mode 100644 index 0000000000..1e9430e047 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/EmbeddedRefColNamesTest.java @@ -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); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/PostalCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/PostalCode.java new file mode 100644 index 0000000000..e0e8593c89 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/PostalCode.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Region.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Region.java new file mode 100644 index 0000000000..7fd5334767 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Region.java @@ -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; + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Town.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Town.java new file mode 100644 index 0000000000..b9d06a29ef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/Town.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/TownCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/TownCode.java new file mode 100644 index 0000000000..e1431f0e3d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embedded/TownCode.java @@ -0,0 +1,8 @@ +package org.hibernate.orm.test.annotations.refcolnames.embedded; + +import jakarta.persistence.Embeddable; + +@Embeddable +class TownCode extends PostalCode { + String town; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/EmbeddedIdRefColNamesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/EmbeddedIdRefColNamesTest.java new file mode 100644 index 0000000000..eee941e20d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/EmbeddedIdRefColNamesTest.java @@ -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); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/PostalCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/PostalCode.java new file mode 100644 index 0000000000..1c3f1d0da0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/PostalCode.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Region.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Region.java new file mode 100644 index 0000000000..99718dd4b5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Region.java @@ -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; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Town.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Town.java new file mode 100644 index 0000000000..59c247e119 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/Town.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/TownCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/TownCode.java new file mode 100644 index 0000000000..1a15db7af3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/embeddedid/TownCode.java @@ -0,0 +1,8 @@ +package org.hibernate.orm.test.annotations.refcolnames.embeddedid; + +import jakarta.persistence.Embeddable; + +@Embeddable +class TownCode extends PostalCode { + String town; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/MixedRefColNamesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/MixedRefColNamesTest.java new file mode 100644 index 0000000000..ea51150c43 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/MixedRefColNamesTest.java @@ -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); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/PostalCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/PostalCode.java new file mode 100644 index 0000000000..2c5bf87e5e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/PostalCode.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Region.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Region.java new file mode 100644 index 0000000000..6f6cde5247 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Region.java @@ -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; + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Town.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Town.java new file mode 100644 index 0000000000..455d7bdd43 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/Town.java @@ -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; +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/TownCode.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/TownCode.java new file mode 100644 index 0000000000..7224a9b685 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/mixed/TownCode.java @@ -0,0 +1,8 @@ +package org.hibernate.orm.test.annotations.refcolnames.mixed; + +import jakarta.persistence.Embeddable; + +@Embeddable +class TownCode extends PostalCode { + String town; +} \ No newline at end of file