diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java index 8971a92728..a5701e26bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -12,8 +12,6 @@ import java.lang.reflect.Member; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.Arrays; -import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -21,12 +19,10 @@ import java.util.Map; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; -import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.TimeZoneStorageStrategy; import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.CollectionTypeRegistration; import org.hibernate.annotations.CollectionTypeRegistrations; import org.hibernate.annotations.Columns; @@ -37,7 +33,6 @@ import org.hibernate.annotations.ConverterRegistration; import org.hibernate.annotations.ConverterRegistrations; import org.hibernate.annotations.EmbeddableInstantiatorRegistration; import org.hibernate.annotations.EmbeddableInstantiatorRegistrations; -import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchProfile; import org.hibernate.annotations.FetchProfiles; import org.hibernate.annotations.FilterDef; @@ -52,11 +47,8 @@ import org.hibernate.annotations.JavaTypeRegistrations; import org.hibernate.annotations.JdbcTypeRegistration; import org.hibernate.annotations.JdbcTypeRegistrations; import org.hibernate.annotations.LazyGroup; -import org.hibernate.annotations.LazyToOne; -import org.hibernate.annotations.LazyToOneOption; import org.hibernate.annotations.ManyToAny; import org.hibernate.annotations.NaturalId; -import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -91,17 +83,14 @@ import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.GenericsHelper; -import org.hibernate.internal.util.StringHelper; import org.hibernate.mapping.Any; import org.hibernate.mapping.Component; import org.hibernate.mapping.Constraint; import org.hibernate.mapping.IdentifierGeneratorCreator; import org.hibernate.mapping.Join; -import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; -import org.hibernate.mapping.ToOne; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.model.convert.internal.JpaAttributeConverterImpl; import org.hibernate.metamodel.spi.EmbeddableInstantiator; @@ -126,14 +115,12 @@ import org.hibernate.usertype.internal.ZonedDateTimeCompositeUserType; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Basic; import jakarta.persistence.Column; -import jakarta.persistence.ConstraintMode; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; import jakarta.persistence.Embedded; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; -import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Inheritance; @@ -153,8 +140,6 @@ import jakarta.persistence.NamedStoredProcedureQueries; import jakarta.persistence.NamedStoredProcedureQuery; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; -import jakarta.persistence.PrimaryKeyJoinColumn; -import jakarta.persistence.PrimaryKeyJoinColumns; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.SequenceGenerators; import jakarta.persistence.SqlResultSetMapping; @@ -168,14 +153,11 @@ import static org.hibernate.cfg.BinderHelper.getOverridableAnnotation; import static org.hibernate.cfg.BinderHelper.getPath; import static org.hibernate.cfg.BinderHelper.getPropertyOverriddenByMapperOrMapsId; import static org.hibernate.cfg.BinderHelper.hasToOneAnnotation; -import static org.hibernate.cfg.BinderHelper.isEmptyAnnotationValue; import static org.hibernate.cfg.BinderHelper.makeIdGenerator; import static org.hibernate.cfg.InheritanceState.getInheritanceStateOfSuperEntity; import static org.hibernate.cfg.InheritanceState.getSuperclassInheritanceState; import static org.hibernate.cfg.PropertyHolderBuilder.buildPropertyHolder; import static org.hibernate.internal.CoreLogging.messageLogger; -import static org.hibernate.internal.util.StringHelper.nullIfEmpty; -import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; /** @@ -1176,7 +1158,7 @@ public final class AnnotationBinder { } else { if ( property.isAnnotationPresent( ManyToOne.class ) ) { - bindManyToOne( + ToOneBinder.bindManyToOne( propertyHolder, inferredData, isIdentifierMapper, @@ -1189,7 +1171,7 @@ public final class AnnotationBinder { ); } else if ( property.isAnnotationPresent( OneToOne.class ) ) { - bindOneToOne( + ToOneBinder.bindOneToOne( propertyHolder, inferredData, isIdentifierMapper, @@ -1596,7 +1578,7 @@ public final class AnnotationBinder { } } bindAny( - getCascadeStrategy( null, hibernateCascade, false, forcePersist), + BinderHelper.getCascadeStrategy( null, hibernateCascade, false, forcePersist), //@Any has not cascade attribute joinColumns, onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(), @@ -1609,126 +1591,6 @@ public final class AnnotationBinder { ); } - private static void bindOneToOne(PropertyHolder propertyHolder, PropertyData inferredData, boolean isIdentifierMapper, boolean inSecondPass, MetadataBuildingContext context, XProperty property, AnnotatedJoinColumn[] joinColumns, PropertyBinder propertyBinder, boolean forcePersist) { - OneToOne ann = property.getAnnotation( OneToOne.class ); - - //check validity - if ( property.isAnnotationPresent( Column.class ) - || property.isAnnotationPresent( Columns.class ) ) { - throw new AnnotationException( - "Property '"+ getPath( propertyHolder, inferredData ) - + "' is a '@OneToOne' association and may not use '@Column' to specify column mappings (use '@PrimaryKeyJoinColumn' instead)" - ); - } - - //FIXME support a proper PKJCs - boolean trueOneToOne = property.isAnnotationPresent( PrimaryKeyJoinColumn.class ) - || property.isAnnotationPresent( PrimaryKeyJoinColumns.class ); - Cascade hibernateCascade = property.getAnnotation( Cascade.class ); - NotFound notFound = property.getAnnotation( NotFound.class ); - NotFoundAction notFoundAction = notFound == null ? null : notFound.action(); - final boolean hasNotFoundAction = notFoundAction != null; - - // MapsId means the columns belong to the pk; - // A @MapsId association (obviously) must be non-null when the entity is first persisted. - // If a @MapsId association is not mapped with @NotFound(IGNORE), then the association - // is mandatory (even if the association has optional=true). - // If a @MapsId association has optional=true and is mapped with @NotFound(IGNORE) then - // the association is optional. - // @OneToOne(optional = true) with @PKJC makes the association optional. - final boolean mandatory = !ann.optional() - || property.isAnnotationPresent( Id.class ) - || property.isAnnotationPresent( MapsId.class ) && !hasNotFoundAction; - matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), notFoundAction, ann.fetch() ); - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - JoinTable assocTable = propertyHolder.getJoinTable(property); - if ( assocTable != null ) { - Join join = propertyHolder.addJoin( assocTable, false ); - if ( hasNotFoundAction ) { - join.disableForeignKeyCreation(); - } - for ( AnnotatedJoinColumn joinColumn : joinColumns) { - joinColumn.setExplicitTableName( join.getTable().getName() ); - } - } - bindOneToOne( - getCascadeStrategy( ann.cascade(), hibernateCascade, ann.orphanRemoval(), forcePersist), - joinColumns, - !mandatory, - getFetchMode( ann.fetch() ), - notFoundAction, - onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(), - ToOneBinder.getTargetEntity(inferredData, context), - propertyHolder, - inferredData, - ann.mappedBy(), - trueOneToOne, - isIdentifierMapper, - inSecondPass, - propertyBinder, - context - ); - } - - private static void bindManyToOne( - PropertyHolder propertyHolder, - PropertyData inferredData, - boolean isIdentifierMapper, - boolean inSecondPass, - MetadataBuildingContext context, - XProperty property, - AnnotatedJoinColumn[] joinColumns, - PropertyBinder propertyBinder, - boolean forcePersist) { - ManyToOne ann = property.getAnnotation( ManyToOne.class ); - - //check validity - if ( property.isAnnotationPresent( Column.class ) - || property.isAnnotationPresent( Columns.class ) ) { - throw new AnnotationException( - "Property '"+ getPath( propertyHolder, inferredData ) - + "' is a '@ManyToOne' association and may not use '@Column' to specify column mappings (use '@JoinColumn' instead)" - ); - } - - Cascade hibernateCascade = property.getAnnotation( Cascade.class ); - NotFound notFound = property.getAnnotation( NotFound.class ); - NotFoundAction notFoundAction = notFound == null ? null : notFound.action(); - matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), notFoundAction, ann.fetch() ); - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - JoinTable assocTable = propertyHolder.getJoinTable(property); - if ( assocTable != null ) { - Join join = propertyHolder.addJoin( assocTable, false ); - for ( AnnotatedJoinColumn joinColumn : joinColumns) { - joinColumn.setExplicitTableName( join.getTable().getName() ); - } - } - // MapsId means the columns belong to the pk; - // A @MapsId association (obviously) must be non-null when the entity is first persisted. - // If a @MapsId association is not mapped with @NotFound(IGNORE), then the association - // is mandatory (even if the association has optional=true). - // If a @MapsId association has optional=true and is mapped with @NotFound(IGNORE) then - // the association is optional. - final boolean mandatory = !ann.optional() - || property.isAnnotationPresent( Id.class ) - || property.isAnnotationPresent( MapsId.class ) && notFoundAction != null; - bindManyToOne( - getCascadeStrategy( ann.cascade(), hibernateCascade, false, forcePersist), - joinColumns, - !mandatory, - notFoundAction, - onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(), - ToOneBinder.getTargetEntity(inferredData, context), - propertyHolder, - inferredData, - false, - isIdentifierMapper, - inSecondPass, - propertyBinder, - context - ); - } - private static void addIndexes( boolean inSecondPass, XProperty property, @@ -2335,287 +2197,7 @@ public final class AnnotationBinder { return baseClassElements.get( 0 ); } - private static void bindManyToOne( - String cascadeStrategy, - AnnotatedJoinColumn[] columns, - boolean optional, - NotFoundAction notFoundAction, - boolean cascadeOnDelete, - XClass targetEntity, - PropertyHolder propertyHolder, - PropertyData inferredData, - boolean unique, - boolean isIdentifierMapper, - boolean inSecondPass, - PropertyBinder propertyBinder, - MetadataBuildingContext context) { - //All FK columns should be in the same table - final org.hibernate.mapping.ManyToOne value = new org.hibernate.mapping.ManyToOne( context, columns[0].getTable() ); - // This is a @OneToOne mapped to a physical o.h.mapping.ManyToOne - if ( unique ) { - value.markAsLogicalOneToOne(); - } - value.setReferencedEntityName( ToOneBinder.getReferenceEntityName( inferredData, targetEntity, context ) ); - final XProperty property = inferredData.getProperty(); - defineFetchingStrategy( value, property ); - //value.setFetchMode( fetchMode ); - value.setNotFoundAction( notFoundAction ); - value.setCascadeDeleteEnabled( cascadeOnDelete ); - //value.setLazy( fetchMode != FetchMode.JOIN ); - if ( !optional ) { - for ( AnnotatedJoinColumn column : columns ) { - column.setNullable( false ); - } - } - if ( property.isAnnotationPresent( MapsId.class ) ) { - //read only - for ( AnnotatedJoinColumn column : columns ) { - column.setInsertable( false ); - column.setUpdatable( false ); - } - } - final JoinColumn joinColumn = property.getAnnotation( JoinColumn.class ); - final JoinColumns joinColumns = property.getAnnotation( JoinColumns.class ); - - //Make sure that JPA1 key-many-to-one columns are read only too - boolean hasSpecjManyToOne=false; - if ( context.getBuildingOptions().isSpecjProprietarySyntaxEnabled() ) { - String columnName = ""; - for ( XProperty prop : inferredData.getDeclaringClass() - .getDeclaredProperties( AccessType.FIELD.getType() ) ) { - if ( prop.isAnnotationPresent( Id.class ) && prop.isAnnotationPresent( Column.class ) ) { - columnName = prop.getAnnotation( Column.class ).name(); - } - - if ( property.isAnnotationPresent( ManyToOne.class ) && joinColumn != null - && ! isEmptyAnnotationValue( joinColumn.name() ) - && joinColumn.name().equals( columnName ) - && !property.isAnnotationPresent( MapsId.class ) ) { - hasSpecjManyToOne = true; - for ( AnnotatedJoinColumn column : columns ) { - column.setInsertable( false ); - column.setUpdatable( false ); - } - } - } - - } - value.setTypeName( inferredData.getClassOrElementName() ); - final String propertyName = inferredData.getPropertyName(); - value.setTypeUsingReflection( propertyHolder.getClassName(), propertyName ); - - final String fullPath = qualify( propertyHolder.getPath(), propertyName ); - - bindForeignKeyNameAndDefinition( - value, - property, - propertyHolder.getOverriddenForeignKey( fullPath ), - joinColumn, - joinColumns, - context - ); - - final FkSecondPass secondPass = new ToOneFkSecondPass( - value, - columns, - !optional && unique, //cannot have nullable and unique on certain DBs like Derby - propertyHolder.getPersistentClass(), - fullPath, - context - ); - if ( inSecondPass ) { - secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); - } - else { - context.getMetadataCollector().addSecondPass( secondPass ); - } - AnnotatedColumn.checkPropertyConsistency( columns, qualify( propertyHolder.getEntityName(), propertyName ) ); - //PropertyBinder binder = new PropertyBinder(); - propertyBinder.setName( propertyName ); - propertyBinder.setValue( value ); - //binder.setCascade(cascadeStrategy); - if ( isIdentifierMapper ) { - propertyBinder.setInsertable( false ); - propertyBinder.setUpdatable( false ); - } - else if (hasSpecjManyToOne) { - propertyBinder.setInsertable( false ); - propertyBinder.setUpdatable( false ); - } - else { - propertyBinder.setInsertable( columns[0].isInsertable() ); - propertyBinder.setUpdatable( columns[0].isUpdatable() ); - } - propertyBinder.setColumns( columns ); - propertyBinder.setAccessType( inferredData.getDefaultAccess() ); - propertyBinder.setCascade( cascadeStrategy ); - propertyBinder.setProperty( property ); - propertyBinder.setXToMany( true ); - - propertyBinder.makePropertyAndBind().setOptional( optional && isNullable( joinColumns, joinColumn ) ); - } - - private static boolean isNullable(JoinColumns joinColumns, JoinColumn joinColumn) { - if ( joinColumn != null ) { - return joinColumn.nullable(); - } - else if ( joinColumns != null ) { - final JoinColumn[] columns = joinColumns.value(); - for ( int i = 0; i < columns.length; i++ ) { - if ( columns[i].nullable() ) { - return true; - } - } - return false; - } - else { - return true; - } - } - - static void defineFetchingStrategy(ToOne toOne, XProperty property) { - final FetchType fetchType = getJpaFetchType( property ); - - LazyToOne lazy = property.getAnnotation( LazyToOne.class ); - NotFound notFound = property.getAnnotation( NotFound.class ); - if ( notFound != null ) { - toOne.setLazy( false ); - toOne.setUnwrapProxy( true ); - } - else if ( lazy != null ) { - toOne.setLazy( lazy.value() != LazyToOneOption.FALSE ); - toOne.setUnwrapProxy( ( lazy.value() == LazyToOneOption.NO_PROXY ) ); - } - else { - toOne.setLazy( fetchType == FetchType.LAZY ); - toOne.setUnwrapProxy( fetchType != FetchType.LAZY ); - toOne.setUnwrapProxyImplicit( true ); - } - - Fetch fetch = property.getAnnotation( Fetch.class ); - if ( fetch != null ) { - // Hibernate @Fetch annotation takes precedence - if ( fetch.value() == org.hibernate.annotations.FetchMode.JOIN ) { - toOne.setFetchMode( FetchMode.JOIN ); - toOne.setLazy( false ); - toOne.setUnwrapProxy( false ); - } - else if ( fetch.value() == org.hibernate.annotations.FetchMode.SELECT ) { - toOne.setFetchMode( FetchMode.SELECT ); - } - else if ( fetch.value() == org.hibernate.annotations.FetchMode.SUBSELECT ) { - throw new AnnotationException( "Association '" + property.getName() - + "' is annotated '@Fetch(SUBSELECT)' but is not many-valued"); - } - else { - throw new AssertionFailure( "Unknown FetchMode: " + fetch.value() ); - } - } - else { - toOne.setFetchMode( getFetchMode( fetchType ) ); - } - } - - private static FetchType getJpaFetchType(XProperty property) { - ManyToOne manyToOne = property.getAnnotation( ManyToOne.class ); - OneToOne oneToOne = property.getAnnotation( OneToOne.class ); - if ( manyToOne != null ) { - return manyToOne.fetch(); - } - else if ( oneToOne != null ) { - return oneToOne.fetch(); - } - else { - throw new AssertionFailure("Define fetch strategy on a property not annotated with @OneToMany nor @OneToOne"); - } - } - - private static void bindOneToOne( - String cascadeStrategy, - AnnotatedJoinColumn[] joinColumns, - boolean optional, - FetchMode fetchMode, - NotFoundAction notFoundAction, - boolean cascadeOnDelete, - XClass targetEntity, - PropertyHolder propertyHolder, - PropertyData inferredData, - String mappedBy, - boolean trueOneToOne, - boolean isIdentifierMapper, - boolean inSecondPass, - PropertyBinder propertyBinder, - MetadataBuildingContext context) { - //column.getTable() => persistentClass.getTable() - final String propertyName = inferredData.getPropertyName(); - LOG.tracev( "Fetching {0} with {1}", propertyName, fetchMode ); - if ( isMapToPK( joinColumns, propertyHolder, trueOneToOne ) || !isEmptyAnnotationValue( mappedBy ) ) { - //is a true one-to-one - //FIXME referencedColumnName ignored => ordering may fail. - OneToOneSecondPass secondPass = new OneToOneSecondPass( - mappedBy, - propertyHolder.getEntityName(), - propertyName, - propertyHolder, - inferredData, - targetEntity, - notFoundAction, - cascadeOnDelete, - optional, - cascadeStrategy, - joinColumns, - context - ); - if ( inSecondPass ) { - secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); - } - else { - context.getMetadataCollector().addSecondPass( secondPass, isEmptyAnnotationValue( mappedBy ) ); - } - } - else { - //has a FK on the table - bindManyToOne( - cascadeStrategy, joinColumns, optional, notFoundAction, cascadeOnDelete, - targetEntity, - propertyHolder, inferredData, true, isIdentifierMapper, inSecondPass, - propertyBinder, context - ); - } - } - - private static boolean isMapToPK(AnnotatedJoinColumn[] joinColumns, PropertyHolder propertyHolder, boolean trueOneToOne) { - if ( trueOneToOne ) { - return true; - } - else { - //try to find a hidden true one to one (FK == PK columns) - KeyValue identifier = propertyHolder.getIdentifier(); - if ( identifier == null ) { - //this is a @OneToOne in an @EmbeddedId (the persistentClass.identifier is not set yet, it's being built) - //by definition the PK cannot refer to itself so it cannot map to itself - return false; - } - else { - List idColumnNames = new ArrayList<>(); - if ( identifier.getColumnSpan() != joinColumns.length ) { - return false; - } - else { - for ( org.hibernate.mapping.Column currentColumn: identifier.getColumns() ) { - idColumnNames.add( currentColumn.getName() ); - } - for ( AnnotatedJoinColumn col: joinColumns) { - if ( !idColumnNames.contains( col.getMappingColumn().getName() ) ) { - return false; - } - } - return true; - } - } - } - } private static void bindAny( String cascadeStrategy, @@ -2669,148 +2251,6 @@ public final class AnnotationBinder { propertyHolder.addProperty( prop, columns, inferredData.getDeclaringClass() ); } - private static EnumSet convertToHibernateCascadeType(jakarta.persistence.CascadeType[] ejbCascades) { - final EnumSet cascadeTypes = EnumSet.noneOf( CascadeType.class ); - if ( ejbCascades != null && ejbCascades.length > 0 ) { - for ( jakarta.persistence.CascadeType cascade: ejbCascades ) { - cascadeTypes.add( convertCascadeType( cascade ) ); - } - } - return cascadeTypes; - } - - private static CascadeType convertCascadeType(jakarta.persistence.CascadeType cascade) { - switch (cascade) { - case ALL: - return CascadeType.ALL; - case PERSIST: - return CascadeType.PERSIST; - case MERGE: - return CascadeType.MERGE; - case REMOVE: - return CascadeType.REMOVE; - case REFRESH: - return CascadeType.REFRESH; - case DETACH: - return CascadeType.DETACH; - default: - throw new AssertionFailure("unknown cascade type: " + cascade); - } - } - - public static String getCascadeStrategy( - jakarta.persistence.CascadeType[] ejbCascades, - Cascade hibernateCascadeAnnotation, - boolean orphanRemoval, - boolean forcePersist) { - EnumSet cascadeTypes = convertToHibernateCascadeType( ejbCascades ); - CascadeType[] hibernateCascades = hibernateCascadeAnnotation == null ? null : hibernateCascadeAnnotation.value(); - if ( hibernateCascades != null && hibernateCascades.length > 0 ) { - cascadeTypes.addAll( Arrays.asList( hibernateCascades ) ); - } - if ( orphanRemoval ) { - cascadeTypes.add( CascadeType.DELETE_ORPHAN ); - cascadeTypes.add( CascadeType.REMOVE ); - } - if ( forcePersist ) { - cascadeTypes.add( CascadeType.PERSIST ); - } - return renderCascadeTypeList( cascadeTypes ); - } - - private static String renderCascadeTypeList(EnumSet cascadeTypes) { - StringBuilder cascade = new StringBuilder(); - for ( CascadeType cascadeType : cascadeTypes) { - switch ( cascadeType ) { - case ALL: - cascade.append( "," ).append( "all" ); - break; - case SAVE_UPDATE: - cascade.append( "," ).append( "save-update" ); - break; - case PERSIST: - cascade.append( "," ).append( "persist" ); - break; - case MERGE: - cascade.append( "," ).append( "merge" ); - break; - case LOCK: - cascade.append( "," ).append( "lock" ); - break; - case REFRESH: - cascade.append( "," ).append( "refresh" ); - break; - case REPLICATE: - cascade.append( "," ).append( "replicate" ); - break; - case DETACH: - cascade.append( "," ).append( "evict" ); - break; - case DELETE: - case REMOVE: - cascade.append( "," ).append( "delete" ); - break; - case DELETE_ORPHAN: - cascade.append( "," ).append( "delete-orphan" ); - break; - } - } - return cascade.length() > 0 ? cascade.substring( 1 ) : "none"; - } - - public static FetchMode getFetchMode(FetchType fetch) { - return fetch == FetchType.EAGER ? FetchMode.JOIN : FetchMode.SELECT; - } - - public static void bindForeignKeyNameAndDefinition( - SimpleValue value, - XProperty property, - ForeignKey fkOverride, - JoinColumn joinColumn, - JoinColumns joinColumns, - MetadataBuildingContext context) { - if ( property.getAnnotation( NotFound.class ) != null ) { - // supersedes all others - value.disableForeignKey(); - } - else { - if ( joinColumn!=null && noConstraint( joinColumn.foreignKey(), context ) - || joinColumns!=null && noConstraint( joinColumns.foreignKey(), context ) ) { - value.disableForeignKey(); - } - else { - final org.hibernate.annotations.ForeignKey fk = property.getAnnotation( org.hibernate.annotations.ForeignKey.class ); - if ( fk != null && StringHelper.isNotEmpty( fk.name() ) ) { - value.setForeignKeyName( fk.name() ); - } - else { - if ( noConstraint( fkOverride, context) ) { - value.disableForeignKey(); - } - else if ( fkOverride != null ) { - value.setForeignKeyName( nullIfEmpty( fkOverride.name() ) ); - value.setForeignKeyDefinition( nullIfEmpty( fkOverride.foreignKeyDefinition() ) ); - } - else if ( joinColumns != null ) { - value.setForeignKeyName( nullIfEmpty( joinColumns.foreignKey().name() ) ); - value.setForeignKeyDefinition( nullIfEmpty( joinColumns.foreignKey().foreignKeyDefinition() ) ); - } - else if ( joinColumn != null ) { - value.setForeignKeyName( nullIfEmpty( joinColumn.foreignKey().name() ) ); - value.setForeignKeyDefinition( nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); - } - } - } - } - } - - private static boolean noConstraint(ForeignKey joinColumns, MetadataBuildingContext context) { - return joinColumns != null - && ( joinColumns.value() == ConstraintMode.NO_CONSTRAINT - || joinColumns.value() == ConstraintMode.PROVIDER_DEFAULT - && context.getBuildingOptions().isNoConstraintByDefault() ); - } - public static HashMap buildGenerators( XAnnotatedElement annElt, MetadataBuildingContext context) { @@ -2942,7 +2382,7 @@ public final class AnnotationBinder { || property.isAnnotationPresent(ManyToMany.class); } - private static void matchIgnoreNotFoundWithFetchType( + static void matchIgnoreNotFoundWithFetchType( String entity, String association, NotFoundAction notFoundAction, 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 c1227ca2aa..15cf84c849 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java @@ -12,6 +12,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -22,13 +23,17 @@ import java.util.StringTokenizer; import java.util.function.Consumer; import java.util.stream.Stream; +import jakarta.persistence.FetchType; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; +import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.annotations.AnyDiscriminatorValue; import org.hibernate.annotations.AnyDiscriminatorValues; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.DialectOverride; import org.hibernate.annotations.Formula; import org.hibernate.annotations.SqlFragmentAlias; @@ -169,20 +174,13 @@ public class BinderHelper { boolean inverse, MetadataBuildingContext context) { - // 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() - // not necessary for a primary key reference - && checkReferencedColumnsType( columns, targetEntity, context ) == NON_PK_REFERENCE ) { - + // this work is not necessary for a primary key reference + if ( checkReferencedColumnsType( columns, targetEntity, context ) == NON_PK_REFERENCE ) { // && !firstColumn.isImplicit() // 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( targetEntity, firstColumn.getReferencedColumn(), context ); + final Object columnOwner = findColumnOwner( targetEntity, columns[0].getReferencedColumn(), context ); checkColumnInSameTable( columns, targetEntity, associatedEntity, context, columnOwner ); // find all properties mapped to each column final List properties = findPropertiesByColumns( columnOwner, columns, associatedEntity, context ); @@ -203,8 +201,10 @@ public class BinderHelper { targetEntity, value, inverse, - firstColumn.getPropertyHolder().getPersistentClass(), - firstColumn.getPropertyName(), + associatedEntity, + propertyName, +// columns[0].getPropertyHolder().getPersistentClass(), +// columns[0].getPropertyName(), property.getName(), context ); @@ -1261,4 +1261,98 @@ public class BinderHelper { } return element.getAnnotation( annotationType ); } + + public static FetchMode getFetchMode(FetchType fetch) { + return fetch == FetchType.EAGER ? FetchMode.JOIN : FetchMode.SELECT; + } + + private static CascadeType convertCascadeType(jakarta.persistence.CascadeType cascade) { + switch (cascade) { + case ALL: + return CascadeType.ALL; + case PERSIST: + return CascadeType.PERSIST; + case MERGE: + return CascadeType.MERGE; + case REMOVE: + return CascadeType.REMOVE; + case REFRESH: + return CascadeType.REFRESH; + case DETACH: + return CascadeType.DETACH; + default: + throw new AssertionFailure("unknown cascade type: " + cascade); + } + } + + private static EnumSet convertToHibernateCascadeType(jakarta.persistence.CascadeType[] ejbCascades) { + final EnumSet cascadeTypes = EnumSet.noneOf( CascadeType.class ); + if ( ejbCascades != null && ejbCascades.length > 0 ) { + for ( jakarta.persistence.CascadeType cascade: ejbCascades ) { + cascadeTypes.add( convertCascadeType( cascade ) ); + } + } + return cascadeTypes; + } + + public static String getCascadeStrategy( + jakarta.persistence.CascadeType[] ejbCascades, + Cascade hibernateCascadeAnnotation, + boolean orphanRemoval, + boolean forcePersist) { + EnumSet cascadeTypes = convertToHibernateCascadeType( ejbCascades ); + CascadeType[] hibernateCascades = hibernateCascadeAnnotation == null ? null : hibernateCascadeAnnotation.value(); + if ( hibernateCascades != null && hibernateCascades.length > 0 ) { + cascadeTypes.addAll( Arrays.asList( hibernateCascades ) ); + } + if ( orphanRemoval ) { + cascadeTypes.add( CascadeType.DELETE_ORPHAN ); + cascadeTypes.add( CascadeType.REMOVE ); + } + if ( forcePersist ) { + cascadeTypes.add( CascadeType.PERSIST ); + } + return renderCascadeTypeList( cascadeTypes ); + } + + private static String renderCascadeTypeList(EnumSet cascadeTypes) { + StringBuilder cascade = new StringBuilder(); + for ( CascadeType cascadeType : cascadeTypes) { + switch ( cascadeType ) { + case ALL: + cascade.append( "," ).append( "all" ); + break; + case SAVE_UPDATE: + cascade.append( "," ).append( "save-update" ); + break; + case PERSIST: + cascade.append( "," ).append( "persist" ); + break; + case MERGE: + cascade.append( "," ).append( "merge" ); + break; + case LOCK: + cascade.append( "," ).append( "lock" ); + break; + case REFRESH: + cascade.append( "," ).append( "refresh" ); + break; + case REPLICATE: + cascade.append( "," ).append( "replicate" ); + break; + case DETACH: + cascade.append( "," ).append( "evict" ); + break; + case DELETE: + case REMOVE: + cascade.append( "," ).append( "delete" ); + break; + case DELETE_ORPHAN: + cascade.append( "," ).append( "delete-orphan" ); + break; + } + } + return cascade.length() > 0 ? cascade.substring( 1 ) : "none"; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java b/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java index da4635b568..6d21976614 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java @@ -8,20 +8,21 @@ package org.hibernate.cfg; import java.util.Map; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinColumns; +import jakarta.persistence.ForeignKey; import org.hibernate.AnnotationException; import org.hibernate.MappingException; import org.hibernate.annotations.LazyGroup; import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.cfg.annotations.PropertyBinder; import org.hibernate.mapping.Column; import org.hibernate.mapping.Component; import org.hibernate.mapping.DependantValue; import org.hibernate.mapping.Join; +import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.ManyToOne; import org.hibernate.mapping.OneToOne; import org.hibernate.mapping.PersistentClass; @@ -32,11 +33,13 @@ import org.hibernate.type.ForeignKeyDirection; import static org.hibernate.cfg.BinderHelper.findPropertyByName; import static org.hibernate.cfg.BinderHelper.getPath; import static org.hibernate.cfg.BinderHelper.isEmptyAnnotationValue; +import static org.hibernate.cfg.ToOneBinder.bindForeignKeyNameAndDefinition; +import static org.hibernate.cfg.ToOneBinder.getReferenceEntityName; import static org.hibernate.internal.util.StringHelper.qualify; /** - * We have to handle OneToOne in a second pass because: - * - + * We have to handle {@link jakarta.persistence.OneToOne} associations + * in a second pass. */ public class OneToOneSecondPass implements SecondPass { private final MetadataBuildingContext buildingContext; @@ -82,167 +85,182 @@ public class OneToOneSecondPass implements SecondPass { //TODO refactor this code, there is a lot of duplication in this method public void doSecondPass(Map persistentClasses) throws MappingException { - OneToOne value = new OneToOne( + final OneToOne value = new OneToOne( buildingContext, propertyHolder.getTable(), propertyHolder.getPersistentClass() ); final String propertyName = inferredData.getPropertyName(); value.setPropertyName( propertyName ); - String referencedEntityName = ToOneBinder.getReferenceEntityName( inferredData, targetEntity, buildingContext ); + final String referencedEntityName = getReferenceEntityName( inferredData, targetEntity, buildingContext ); value.setReferencedEntityName( referencedEntityName ); - AnnotationBinder.defineFetchingStrategy( value, inferredData.getProperty() ); + XProperty prop = inferredData.getProperty(); + ToOneBinder.defineFetchingStrategy( value, prop ); //value.setFetchMode( fetchMode ); value.setCascadeDeleteEnabled( cascadeOnDelete ); //value.setLazy( fetchMode != FetchMode.JOIN ); value.setConstrained( !optional ); - final ForeignKeyDirection foreignKeyDirection = !isEmptyAnnotationValue( mappedBy ) - ? ForeignKeyDirection.TO_PARENT - : ForeignKeyDirection.FROM_PARENT; - value.setForeignKeyType(foreignKeyDirection); - AnnotationBinder.bindForeignKeyNameAndDefinition( - value, - inferredData.getProperty(), - inferredData.getProperty().getAnnotation( jakarta.persistence.ForeignKey.class ), - inferredData.getProperty().getAnnotation( JoinColumn.class ), - inferredData.getProperty().getAnnotation( JoinColumns.class), - buildingContext - ); + value.setForeignKeyType( getForeignKeyDirection() ); + bindForeignKeyNameAndDefinition( value, prop, prop.getAnnotation( ForeignKey.class ), buildingContext ); - PropertyBinder binder = new PropertyBinder(); + final PropertyBinder binder = new PropertyBinder(); binder.setName( propertyName ); - binder.setProperty( inferredData.getProperty() ); + binder.setProperty(prop); binder.setValue( value ); binder.setCascade( cascadeStrategy ); binder.setAccessType( inferredData.getDefaultAccess() ); - final LazyGroup lazyGroupAnnotation = inferredData.getProperty().getAnnotation( LazyGroup.class ); + final LazyGroup lazyGroupAnnotation = prop.getAnnotation( LazyGroup.class ); if ( lazyGroupAnnotation != null ) { binder.setLazyGroup( lazyGroupAnnotation.value() ); } - Property property = binder.makeProperty(); + final Property property = binder.makeProperty(); property.setOptional( optional ); if ( isEmptyAnnotationValue( mappedBy ) ) { - // we need to check if the columns are in the right order - // if not, then we need to create a many to one and formula - // but actually, since entities linked by a one to one need - // to share the same composite id class, this cannot happen - boolean rightOrder = true; - - if ( rightOrder ) { - final ToOneFkSecondPass secondPass = new ToOneFkSecondPass( - value, - joinColumns, - !optional, //cannot have nullable and unique on certain DBs - propertyHolder.getPersistentClass(), - qualify( propertyHolder.getPath(), propertyName ), - buildingContext - ); - secondPass.doSecondPass( persistentClasses ); - //no column associated since its a one to one - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); - } -// else { - //this is a many to one with Formula -// } + bindUnowned( persistentClasses, value, propertyName, property ); } else { - value.setMappedByProperty( mappedBy ); - PersistentClass otherSide = persistentClasses.get( value.getReferencedEntityName() ); - if ( otherSide == null ) { - throw new MappingException( "Association '" + getPath( propertyHolder, inferredData ) - + "' targets unknown entity type '" + value.getReferencedEntityName() + "'" ); - } - Property otherSideProperty; - try { - otherSideProperty = findPropertyByName( otherSide, mappedBy ); - } - catch (MappingException e) { - otherSideProperty = null; - } - if ( otherSideProperty == null ) { - throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData ) - + "' is 'mappedBy' a property named '" + mappedBy - + "' which does not exist in the target entity type '" + value.getReferencedEntityName() + "'" ); - } - if ( otherSideProperty.getValue() instanceof OneToOne ) { - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); - } - else if ( otherSideProperty.getValue() instanceof ManyToOne ) { - Join otherSideJoin = null; - for ( Join otherSideJoinValue : otherSide.getJoins() ) { - if ( otherSideJoinValue.containsProperty( otherSideProperty ) ) { - otherSideJoin = otherSideJoinValue; - break; - } - } - if ( otherSideJoin != null ) { - //@OneToOne @JoinTable - Join mappedByJoin = buildJoinFromMappedBySide( - persistentClasses.get( ownerEntity ), otherSideProperty, otherSideJoin - ); - ManyToOne manyToOne = new ManyToOne( buildingContext, mappedByJoin.getTable() ); - //FIXME use ignore not found here - manyToOne.setNotFoundAction( notFoundAction ); - manyToOne.setCascadeDeleteEnabled( value.isCascadeDeleteEnabled() ); - manyToOne.setFetchMode( value.getFetchMode() ); - manyToOne.setLazy( value.isLazy() ); - manyToOne.setReferencedEntityName( value.getReferencedEntityName() ); - manyToOne.setUnwrapProxy( value.isUnwrapProxy() ); - manyToOne.markAsLogicalOneToOne(); - property.setValue( manyToOne ); - for ( Column column: otherSideJoin.getKey().getColumns() ) { - Column copy = new Column(); - copy.setLength( column.getLength() ); - copy.setScale( column.getScale() ); - copy.setValue( manyToOne ); - copy.setName( column.getQuotedName() ); - copy.setNullable( column.isNullable() ); - copy.setPrecision( column.getPrecision() ); - copy.setUnique( column.isUnique() ); - copy.setSqlType( column.getSqlType() ); - copy.setCheckConstraint( column.getCheckConstraint() ); - copy.setComment( column.getComment() ); - copy.setDefaultValue( column.getDefaultValue() ); - copy.setGeneratedAs(column.getGeneratedAs() ); - manyToOne.addColumn( copy ); - } - mappedByJoin.addProperty( property ); - } - else { - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); - } - - value.setReferencedPropertyName( mappedBy ); - - // HHH-6813 - // Foo: @Id long id, @OneToOne(mappedBy="foo") Bar bar - // Bar: @Id @OneToOne Foo foo - boolean referenceToPrimaryKey = mappedBy == null - || otherSide.getIdentifier() instanceof Component - && ! ( (Component) otherSide.getIdentifier() ).hasProperty( mappedBy ); - value.setReferenceToPrimaryKey( referenceToPrimaryKey ); - - String propertyRef = value.getReferencedPropertyName(); - if ( propertyRef != null ) { - buildingContext.getMetadataCollector().addUniquePropertyReference( - value.getReferencedEntityName(), - propertyRef - ); - } - } - else { - throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData ) - + "' is 'mappedBy' a property named '" + mappedBy - + "' of the target entity type '" + value.getReferencedEntityName() - + "' which is not a '@OneToOne' or '@ManyToOne' association" ); - } + bindOwned( persistentClasses, value, property ); } value.sortProperties(); } + private ForeignKeyDirection getForeignKeyDirection() { + return !isEmptyAnnotationValue( mappedBy ) ? ForeignKeyDirection.TO_PARENT : ForeignKeyDirection.FROM_PARENT; + } + + private void bindOwned(Map persistentClasses, OneToOne value, Property property) { + value.setMappedByProperty( mappedBy ); + final PersistentClass targetEntity = persistentClasses.get( value.getReferencedEntityName() ); + if ( targetEntity == null ) { + throw new MappingException( "Association '" + getPath( propertyHolder, inferredData ) + + "' targets unknown entity type '" + value.getReferencedEntityName() + "'" ); + } + final Property targetProperty = targetProperty( value, targetEntity ); + if ( targetProperty.getValue() instanceof OneToOne ) { + propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); + } + else if ( targetProperty.getValue() instanceof ManyToOne ) { + bindTargetManyToOne( persistentClasses, value, property, targetEntity, targetProperty ); + } + else { + throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData ) + + "' is 'mappedBy' a property named '" + mappedBy + + "' of the target entity type '" + value.getReferencedEntityName() + + "' which is not a '@OneToOne' or '@ManyToOne' association" ); + } + } + + private void bindTargetManyToOne( + Map persistentClasses, + OneToOne value, + Property property, + PersistentClass targetEntity, + Property targetProperty) { + Join otherSideJoin = null; + for ( Join otherSideJoinValue : targetEntity.getJoins() ) { + if ( otherSideJoinValue.containsProperty(targetProperty) ) { + otherSideJoin = otherSideJoinValue; + break; + } + } + if ( otherSideJoin != null ) { + //@OneToOne @JoinTable + final Join mappedByJoin = buildJoinFromMappedBySide( + persistentClasses.get( ownerEntity ), targetProperty, otherSideJoin + ); + final ManyToOne manyToOne = new ManyToOne( buildingContext, mappedByJoin.getTable() ); + //FIXME use ignore not found here + manyToOne.setNotFoundAction( notFoundAction ); + manyToOne.setCascadeDeleteEnabled( value.isCascadeDeleteEnabled() ); + manyToOne.setFetchMode( value.getFetchMode() ); + manyToOne.setLazy( value.isLazy() ); + manyToOne.setReferencedEntityName( value.getReferencedEntityName() ); + manyToOne.setUnwrapProxy( value.isUnwrapProxy() ); + manyToOne.markAsLogicalOneToOne(); + property.setValue( manyToOne ); + for ( Column column: otherSideJoin.getKey().getColumns() ) { + Column copy = new Column(); + copy.setLength( column.getLength() ); + copy.setScale( column.getScale() ); + copy.setValue( manyToOne ); + copy.setName( column.getQuotedName() ); + copy.setNullable( column.isNullable() ); + copy.setPrecision( column.getPrecision() ); + copy.setUnique( column.isUnique() ); + copy.setSqlType( column.getSqlType() ); + copy.setCheckConstraint( column.getCheckConstraint() ); + copy.setComment( column.getComment() ); + copy.setDefaultValue( column.getDefaultValue() ); + copy.setGeneratedAs( column.getGeneratedAs() ); + manyToOne.addColumn( copy ); + } + mappedByJoin.addProperty( property ); + } + else { + propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); + } + + value.setReferencedPropertyName( mappedBy ); + + // HHH-6813 + // Foo: @Id long id, @OneToOne(mappedBy="foo") Bar bar + // Bar: @Id @OneToOne Foo foo + final KeyValue targetEntityIdentifier = targetEntity.getIdentifier(); + boolean referenceToPrimaryKey = mappedBy == null + || targetEntityIdentifier instanceof Component + && !( (Component) targetEntityIdentifier ).hasProperty( mappedBy ); + value.setReferenceToPrimaryKey( referenceToPrimaryKey ); + + final String propertyRef = value.getReferencedPropertyName(); + if ( propertyRef != null ) { + buildingContext.getMetadataCollector() + .addUniquePropertyReference( value.getReferencedEntityName(), propertyRef ); + } + } + + private Property targetProperty(OneToOne value, PersistentClass targetEntity) { + try { + Property targetProperty = findPropertyByName( targetEntity, mappedBy ); + if ( targetProperty != null ) { + return targetProperty; + } + } + catch (MappingException e) { + // swallow it + } + throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData ) + + "' is 'mappedBy' a property named '" + mappedBy + + "' which does not exist in the target entity type '" + value.getReferencedEntityName() + "'" ); + } + + private void bindUnowned(Map persistentClasses, OneToOne value, String propertyName, Property property) { + // we need to check if the columns are in the right order + // if not, then we need to create a many to one and formula + // but actually, since entities linked by a one to one need + // to share the same composite id class, this cannot happen + boolean rightOrder = true; + + if ( rightOrder ) { + final ToOneFkSecondPass secondPass = new ToOneFkSecondPass( + value, + joinColumns, + !optional, //cannot have nullable and unique on certain DBs + propertyHolder.getPersistentClass(), + qualify( propertyHolder.getPath(), propertyName), + buildingContext + ); + secondPass.doSecondPass(persistentClasses); + //no column associated since its a one to one + propertyHolder.addProperty(property, inferredData.getDeclaringClass() ); + } +// else { + // this is a @ManyToOne with Formula +// } + } + /** * Builds the Join instance for the mapped by side of a OneToOne association using * a join table. diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/ToOneBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/ToOneBinder.java index a69f7485e7..00c49f04d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/ToOneBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/ToOneBinder.java @@ -6,24 +6,562 @@ */ package org.hibernate.cfg; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; import jakarta.persistence.OneToOne; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.PrimaryKeyJoinColumns; +import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; +import org.hibernate.FetchMode; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.Columns; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.cfg.annotations.PropertyBinder; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.mapping.Join; +import org.hibernate.mapping.KeyValue; +import org.hibernate.mapping.SimpleValue; +import org.hibernate.mapping.ToOne; + +import java.util.ArrayList; +import java.util.List; + +import static org.hibernate.cfg.AnnotatedColumn.checkPropertyConsistency; +import static org.hibernate.cfg.AnnotationBinder.matchIgnoreNotFoundWithFetchType; +import static org.hibernate.cfg.BinderHelper.getCascadeStrategy; +import static org.hibernate.cfg.BinderHelper.getFetchMode; +import static org.hibernate.cfg.BinderHelper.getPath; +import static org.hibernate.cfg.BinderHelper.isEmptyAnnotationValue; +import static org.hibernate.internal.CoreLogging.messageLogger; +import static org.hibernate.internal.util.StringHelper.isNotEmpty; +import static org.hibernate.internal.util.StringHelper.nullIfEmpty; +import static org.hibernate.internal.util.StringHelper.qualify; /** - * Work in progress - * The goal of this class is to aggregate all operations - * related to ToOne binding operations - * * @author Emmanuel Bernard */ public class ToOneBinder { - public static String getReferenceEntityName(PropertyData propertyData, XClass targetEntity, MetadataBuildingContext buildingContext) { - if ( AnnotationBinder.isDefault( targetEntity, buildingContext ) ) { + private static final CoreMessageLogger LOG = messageLogger( ToOneBinder.class ); + + static void bindManyToOne( + PropertyHolder propertyHolder, + PropertyData inferredData, + boolean isIdentifierMapper, + boolean inSecondPass, + MetadataBuildingContext context, + XProperty property, + AnnotatedJoinColumn[] joinColumns, + PropertyBinder propertyBinder, + boolean forcePersist) { + final ManyToOne manyToOne = property.getAnnotation( ManyToOne.class ); + + //check validity + if ( property.isAnnotationPresent( Column.class ) + || property.isAnnotationPresent( Columns.class ) ) { + throw new AnnotationException( + "Property '"+ getPath( propertyHolder, inferredData ) + + "' is a '@ManyToOne' association and may not use '@Column' to specify column mappings (use '@JoinColumn' instead)" + ); + } + + final Cascade hibernateCascade = property.getAnnotation( Cascade.class ); + final NotFound notFound = property.getAnnotation( NotFound.class ); + final NotFoundAction notFoundAction = notFound == null ? null : notFound.action(); + matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), notFoundAction, manyToOne.fetch() ); + final OnDelete onDelete = property.getAnnotation( OnDelete.class ); + final JoinTable joinTable = propertyHolder.getJoinTable( property ); + if ( joinTable != null ) { + final Join join = propertyHolder.addJoin( joinTable, false ); + for ( AnnotatedJoinColumn joinColumn : joinColumns ) { + joinColumn.setExplicitTableName( join.getTable().getName() ); + } + } + final boolean mandatory = isMandatory( manyToOne.optional(), property, notFoundAction ); + bindManyToOne( + getCascadeStrategy( manyToOne.cascade(), hibernateCascade, false, forcePersist ), + joinColumns, + !mandatory, + notFoundAction, + onDelete != null && OnDeleteAction.CASCADE == onDelete.action(), + getTargetEntity( inferredData, context ), + propertyHolder, + inferredData, + false, + isIdentifierMapper, + inSecondPass, + propertyBinder, + context + ); + } + + private static boolean isMandatory(boolean optional, XProperty property, NotFoundAction notFoundAction) { + // @MapsId means the columns belong to the pk; + // A @MapsId association (obviously) must be non-null when the entity is first persisted. + // If a @MapsId association is not mapped with @NotFound(IGNORE), then the association + // is mandatory (even if the association has optional=true). + // If a @MapsId association has optional=true and is mapped with @NotFound(IGNORE) then + // the association is optional. + // @OneToOne(optional = true) with @PKJC makes the association optional. + return !optional + || property.isAnnotationPresent( Id.class ) + || property.isAnnotationPresent( MapsId.class ) && notFoundAction != NotFoundAction.IGNORE; + } + + private static void bindManyToOne( + String cascadeStrategy, + AnnotatedJoinColumn[] columns, + boolean optional, + NotFoundAction notFoundAction, + boolean cascadeOnDelete, + XClass targetEntity, + PropertyHolder propertyHolder, + PropertyData inferredData, + boolean unique, // identifies a "logical" @OneToOne + boolean isIdentifierMapper, + boolean inSecondPass, + PropertyBinder propertyBinder, + MetadataBuildingContext context) { + // All FK columns should be in the same table + final org.hibernate.mapping.ManyToOne value = + new org.hibernate.mapping.ManyToOne( context, columns[0].getTable() ); + if ( unique ) { + // This is a @OneToOne mapped to a physical o.h.mapping.ManyToOne + value.markAsLogicalOneToOne(); + } + value.setReferencedEntityName( getReferenceEntityName( inferredData, targetEntity, context ) ); + final XProperty property = inferredData.getProperty(); + defineFetchingStrategy( value, property ); + //value.setFetchMode( fetchMode ); + value.setNotFoundAction( notFoundAction ); + value.setCascadeDeleteEnabled( cascadeOnDelete ); + //value.setLazy( fetchMode != FetchMode.JOIN ); + if ( !optional ) { + for ( AnnotatedJoinColumn column : columns ) { + column.setNullable( false ); + } + } + + if ( property.isAnnotationPresent( MapsId.class ) ) { + //read only + for ( AnnotatedJoinColumn column : columns ) { + column.setInsertable( false ); + column.setUpdatable( false ); + } + } + + boolean hasSpecjManyToOne = handleSpecjSyntax( columns, inferredData, context, property ); + value.setTypeName( inferredData.getClassOrElementName() ); + final String propertyName = inferredData.getPropertyName(); + value.setTypeUsingReflection( propertyHolder.getClassName(), propertyName ); + + final String fullPath = qualify( propertyHolder.getPath(), propertyName ); + + bindForeignKeyNameAndDefinition( value, property, propertyHolder.getOverriddenForeignKey( fullPath ), context ); + + final FkSecondPass secondPass = new ToOneFkSecondPass( + value, + columns, + !optional && unique, //cannot have nullable and unique on certain DBs like Derby + propertyHolder.getPersistentClass(), + fullPath, + context + ); + if ( inSecondPass ) { + secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); + } + else { + context.getMetadataCollector().addSecondPass( secondPass ); + } + + processManyToOneProperty( + cascadeStrategy, + columns, + optional, + propertyHolder, + inferredData, + isIdentifierMapper, + propertyBinder, + value, + property, + hasSpecjManyToOne, + propertyName + ); + } + + private static boolean handleSpecjSyntax( + AnnotatedJoinColumn[] columns, + PropertyData inferredData, + MetadataBuildingContext context, + XProperty property) { + //Make sure that JPA1 key-many-to-one columns are read only too + boolean hasSpecjManyToOne = false; + if ( context.getBuildingOptions().isSpecjProprietarySyntaxEnabled() ) { + final JoinColumn joinColumn = property.getAnnotation( JoinColumn.class ); + String columnName = ""; + for ( XProperty prop : inferredData.getDeclaringClass() + .getDeclaredProperties( AccessType.FIELD.getType() ) ) { + if ( prop.isAnnotationPresent( Id.class ) && prop.isAnnotationPresent( Column.class ) ) { + columnName = prop.getAnnotation( Column.class ).name(); + } + + if ( property.isAnnotationPresent( ManyToOne.class ) && joinColumn != null + && ! isEmptyAnnotationValue( joinColumn.name() ) + && joinColumn.name().equals( columnName ) + && !property.isAnnotationPresent( MapsId.class ) ) { + hasSpecjManyToOne = true; + for ( AnnotatedJoinColumn column : columns) { + column.setInsertable( false ); + column.setUpdatable( false ); + } + } + } + } + return hasSpecjManyToOne; + } + + private static void processManyToOneProperty( + String cascadeStrategy, + AnnotatedJoinColumn[] columns, + boolean optional, + PropertyHolder propertyHolder, + PropertyData inferredData, + boolean isIdentifierMapper, + PropertyBinder propertyBinder, + org.hibernate.mapping.ManyToOne value, XProperty property, + boolean hasSpecjManyToOne, + String propertyName) { + + checkPropertyConsistency( columns, qualify( propertyHolder.getEntityName(), propertyName ) ); + + //PropertyBinder binder = new PropertyBinder(); + propertyBinder.setName( propertyName ); + propertyBinder.setValue( value ); + //binder.setCascade(cascadeStrategy); + if (isIdentifierMapper) { + propertyBinder.setInsertable( false ); + propertyBinder.setUpdatable( false ); + } + else if (hasSpecjManyToOne) { + propertyBinder.setInsertable( false ); + propertyBinder.setUpdatable( false ); + } + else { + propertyBinder.setInsertable( columns[0].isInsertable() ); + propertyBinder.setUpdatable( columns[0].isUpdatable() ); + } + propertyBinder.setColumns( columns ); + propertyBinder.setAccessType( inferredData.getDefaultAccess() ); + propertyBinder.setCascade( cascadeStrategy ); + propertyBinder.setProperty( property ); + propertyBinder.setXToMany( true ); + + final JoinColumn joinColumn = property.getAnnotation( JoinColumn.class ); + final JoinColumns joinColumns = property.getAnnotation( JoinColumns.class ); + propertyBinder.makePropertyAndBind() + .setOptional( optional && isNullable( joinColumns, joinColumn ) ); + } + + private static boolean isNullable(JoinColumns joinColumns, JoinColumn joinColumn) { + if ( joinColumn != null ) { + return joinColumn.nullable(); + } + else if ( joinColumns != null ) { + for ( JoinColumn column : joinColumns.value() ) { + if ( column.nullable() ) { + return true; + } + } + return false; + } + else { + return true; + } + } + + static void defineFetchingStrategy(ToOne toOne, XProperty property) { + final FetchType fetchType = getJpaFetchType( property ); + + final LazyToOne lazy = property.getAnnotation( LazyToOne.class ); + final NotFound notFound = property.getAnnotation( NotFound.class ); + if ( notFound != null ) { + toOne.setLazy( false ); + toOne.setUnwrapProxy( true ); + } + else if ( lazy != null ) { + toOne.setLazy( lazy.value() != LazyToOneOption.FALSE ); + toOne.setUnwrapProxy( ( lazy.value() == LazyToOneOption.NO_PROXY ) ); + } + else { + toOne.setLazy( fetchType == FetchType.LAZY ); + toOne.setUnwrapProxy( fetchType != FetchType.LAZY ); + toOne.setUnwrapProxyImplicit( true ); + } + + final Fetch fetch = property.getAnnotation( Fetch.class ); + if ( fetch != null ) { + // Hibernate @Fetch annotation takes precedence + if ( fetch.value() == org.hibernate.annotations.FetchMode.JOIN ) { + toOne.setFetchMode( FetchMode.JOIN ); + toOne.setLazy( false ); + toOne.setUnwrapProxy( false ); + } + else if ( fetch.value() == org.hibernate.annotations.FetchMode.SELECT ) { + toOne.setFetchMode( FetchMode.SELECT ); + } + else if ( fetch.value() == org.hibernate.annotations.FetchMode.SUBSELECT ) { + throw new AnnotationException( "Association '" + property.getName() + + "' is annotated '@Fetch(SUBSELECT)' but is not many-valued"); + } + else { + throw new AssertionFailure( "Unknown FetchMode: " + fetch.value() ); + } + } + else { + toOne.setFetchMode( getFetchMode( fetchType ) ); + } + } + + private static FetchType getJpaFetchType(XProperty property) { + final ManyToOne manyToOne = property.getAnnotation( ManyToOne.class ); + final OneToOne oneToOne = property.getAnnotation( OneToOne.class ); + if ( manyToOne != null ) { + return manyToOne.fetch(); + } + else if ( oneToOne != null ) { + return oneToOne.fetch(); + } + else { + throw new AssertionFailure("Define fetch strategy on a property not annotated with @OneToMany nor @OneToOne"); + } + } + + static void bindOneToOne( + PropertyHolder propertyHolder, + PropertyData inferredData, + boolean isIdentifierMapper, + boolean inSecondPass, + MetadataBuildingContext context, + XProperty property, + AnnotatedJoinColumn[] joinColumns, + PropertyBinder propertyBinder, + boolean forcePersist) { + final OneToOne oneToOne = property.getAnnotation( OneToOne.class ); + + //check validity + if ( property.isAnnotationPresent( Column.class ) + || property.isAnnotationPresent( Columns.class ) ) { + throw new AnnotationException( + "Property '"+ getPath( propertyHolder, inferredData ) + + "' is a '@OneToOne' association and may not use '@Column' to specify column mappings" + + " (use '@PrimaryKeyJoinColumn' instead)" + ); + } + + //FIXME support a proper PKJCs + final boolean trueOneToOne = property.isAnnotationPresent( PrimaryKeyJoinColumn.class ) + || property.isAnnotationPresent( PrimaryKeyJoinColumns.class ); + final Cascade hibernateCascade = property.getAnnotation( Cascade.class ); + final NotFound notFound = property.getAnnotation( NotFound.class ); + final NotFoundAction notFoundAction = notFound == null ? null : notFound.action(); + + final boolean mandatory = isMandatory( oneToOne.optional(), property, notFoundAction ); + matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), notFoundAction, oneToOne.fetch() ); + final OnDelete onDelete = property.getAnnotation( OnDelete.class ); + final JoinTable joinTable = propertyHolder.getJoinTable(property); + if ( joinTable != null ) { + final Join join = propertyHolder.addJoin( joinTable, false ); + if ( notFoundAction != null ) { + join.disableForeignKeyCreation(); + } + for ( AnnotatedJoinColumn joinColumn : joinColumns) { + joinColumn.setExplicitTableName( join.getTable().getName() ); + } + } + bindOneToOne( + getCascadeStrategy( oneToOne.cascade(), hibernateCascade, oneToOne.orphanRemoval(), forcePersist ), + joinColumns, + !mandatory, + getFetchMode( oneToOne.fetch() ), + notFoundAction, + onDelete != null && OnDeleteAction.CASCADE == onDelete.action(), + getTargetEntity( inferredData, context ), + propertyHolder, + inferredData, + oneToOne.mappedBy(), + trueOneToOne, + isIdentifierMapper, + inSecondPass, + propertyBinder, + context + ); + } + + private static void bindOneToOne( + String cascadeStrategy, + AnnotatedJoinColumn[] joinColumns, + boolean optional, + FetchMode fetchMode, + NotFoundAction notFoundAction, + boolean cascadeOnDelete, + XClass targetEntity, + PropertyHolder propertyHolder, + PropertyData inferredData, + String mappedBy, + boolean trueOneToOne, + boolean isIdentifierMapper, + boolean inSecondPass, + PropertyBinder propertyBinder, + MetadataBuildingContext context) { + //column.getTable() => persistentClass.getTable() + final String propertyName = inferredData.getPropertyName(); + LOG.tracev( "Fetching {0} with {1}", propertyName, fetchMode ); + if ( isMapToPK( joinColumns, propertyHolder, trueOneToOne ) || !isEmptyAnnotationValue( mappedBy ) ) { + //is a true one-to-one + //FIXME referencedColumnName ignored => ordering may fail. + final OneToOneSecondPass secondPass = new OneToOneSecondPass( + mappedBy, + propertyHolder.getEntityName(), + propertyName, + propertyHolder, + inferredData, + targetEntity, + notFoundAction, + cascadeOnDelete, + optional, + cascadeStrategy, + joinColumns, + context + ); + if ( inSecondPass ) { + secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); + } + else { + context.getMetadataCollector().addSecondPass( secondPass, isEmptyAnnotationValue( mappedBy ) ); + } + } + else { + //has a FK on the table + bindManyToOne( + cascadeStrategy, + joinColumns, + optional, + notFoundAction, + cascadeOnDelete, + targetEntity, + propertyHolder, + inferredData, + true, + isIdentifierMapper, + inSecondPass, + propertyBinder, + context + ); + } + } + + private static boolean isMapToPK(AnnotatedJoinColumn[] joinColumns, PropertyHolder propertyHolder, boolean trueOneToOne) { + if ( trueOneToOne ) { + return true; + } + else { + //try to find a hidden true one to one (FK == PK columns) + KeyValue identifier = propertyHolder.getIdentifier(); + if ( identifier == null ) { + //this is a @OneToOne in an @EmbeddedId (the persistentClass.identifier is not set yet, it's being built) + //by definition the PK cannot refer to itself so it cannot map to itself + return false; + } + else { + List idColumnNames = new ArrayList<>(); + if ( identifier.getColumnSpan() != joinColumns.length ) { + return false; + } + else { + for ( org.hibernate.mapping.Column currentColumn: identifier.getColumns() ) { + idColumnNames.add( currentColumn.getName() ); + } + for ( AnnotatedJoinColumn col: joinColumns) { + if ( !idColumnNames.contains( col.getMappingColumn().getName() ) ) { + return false; + } + } + return true; + } + } + } + } + + public static void bindForeignKeyNameAndDefinition( + SimpleValue value, + XProperty property, + ForeignKey foreignKey, + MetadataBuildingContext context) { + if ( property.getAnnotation( NotFound.class ) != null ) { + // supersedes all others + value.disableForeignKey(); + } + else { + final JoinColumn joinColumn = property.getAnnotation( JoinColumn.class ); + final JoinColumns joinColumns = property.getAnnotation( JoinColumns.class ); + if ( joinColumn!=null && noConstraint( joinColumn.foreignKey(), context ) + || joinColumns!=null && noConstraint( joinColumns.foreignKey(), context ) ) { + value.disableForeignKey(); + } + else { + final org.hibernate.annotations.ForeignKey fk = + property.getAnnotation( org.hibernate.annotations.ForeignKey.class ); + if ( fk != null && isNotEmpty( fk.name() ) ) { + value.setForeignKeyName( fk.name() ); + } + else { + if ( noConstraint( foreignKey, context ) ) { + value.disableForeignKey(); + } + else if ( foreignKey != null ) { + value.setForeignKeyName( nullIfEmpty( foreignKey.name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( foreignKey.foreignKeyDefinition() ) ); + } + else if ( joinColumns != null ) { + value.setForeignKeyName( nullIfEmpty( joinColumns.foreignKey().name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( joinColumns.foreignKey().foreignKeyDefinition() ) ); + } + else if ( joinColumn != null ) { + value.setForeignKeyName( nullIfEmpty( joinColumn.foreignKey().name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); + } + } + } + } + } + + private static boolean noConstraint(ForeignKey joinColumns, MetadataBuildingContext context) { + return joinColumns != null + && ( joinColumns.value() == ConstraintMode.NO_CONSTRAINT + || joinColumns.value() == ConstraintMode.PROVIDER_DEFAULT + && context.getBuildingOptions().isNoConstraintByDefault() ); + } + + public static String getReferenceEntityName(PropertyData propertyData, XClass targetEntity, MetadataBuildingContext context) { + if ( AnnotationBinder.isDefault( targetEntity, context ) ) { return propertyData.getClassOrElementName(); } else { @@ -31,19 +569,16 @@ public class ToOneBinder { } } - public static String getReferenceEntityName(PropertyData propertyData, MetadataBuildingContext buildingContext) { - XClass targetEntity = getTargetEntity( propertyData, buildingContext ); - if ( AnnotationBinder.isDefault( targetEntity, buildingContext ) ) { - return propertyData.getClassOrElementName(); - } - else { - return targetEntity.getName(); - } + public static String getReferenceEntityName(PropertyData propertyData, MetadataBuildingContext context) { + XClass targetEntity = getTargetEntity( propertyData, context ); + return AnnotationBinder.isDefault( targetEntity, context ) + ? propertyData.getClassOrElementName() + : targetEntity.getName(); } - public static XClass getTargetEntity(PropertyData propertyData, MetadataBuildingContext buildingContext) { - XProperty property = propertyData.getProperty(); - return buildingContext.getBootstrapContext().getReflectionManager().toXClass( getTargetEntityClass( property ) ); + public static XClass getTargetEntity(PropertyData propertyData, MetadataBuildingContext context) { + return context.getBootstrapContext().getReflectionManager() + .toXClass( getTargetEntityClass( propertyData.getProperty() ) ); } private static Class getTargetEntityClass(XProperty property) { diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java index c320c60c70..35b4692476 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java @@ -155,10 +155,11 @@ import static org.hibernate.cfg.AnnotatedColumn.checkPropertyConsistency; import static org.hibernate.cfg.AnnotatedJoinColumn.buildJoinColumnsWithDefaultColumnSuffix; import static org.hibernate.cfg.AnnotatedJoinColumn.buildJoinTableJoinColumns; import static org.hibernate.cfg.AnnotationBinder.fillComponent; -import static org.hibernate.cfg.AnnotationBinder.getCascadeStrategy; import static org.hibernate.cfg.BinderHelper.PRIMITIVE_NAMES; import static org.hibernate.cfg.BinderHelper.buildAnyValue; import static org.hibernate.cfg.BinderHelper.createSyntheticPropertyReference; +import static org.hibernate.cfg.BinderHelper.getCascadeStrategy; +import static org.hibernate.cfg.BinderHelper.getFetchMode; import static org.hibernate.cfg.BinderHelper.getOverridableAnnotation; import static org.hibernate.cfg.BinderHelper.getPath; import static org.hibernate.cfg.BinderHelper.isEmptyAnnotationValue; @@ -1416,7 +1417,7 @@ public abstract class CollectionBinder { } } else { - collection.setFetchMode( AnnotationBinder.getFetchMode( jpaFetchType ) ); + collection.setFetchMode( getFetchMode( jpaFetchType ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/ListBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/ListBinder.java index babe9e24fd..ccbf57bc8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/ListBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/ListBinder.java @@ -10,14 +10,10 @@ import java.util.Map; import java.util.function.Supplier; import org.hibernate.MappingException; -import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.OrderBy; -import org.hibernate.annotations.common.reflection.XClass; -import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.cfg.CollectionSecondPass; import org.hibernate.cfg.AnnotatedColumn; -import org.hibernate.cfg.AnnotatedJoinColumn; import org.hibernate.cfg.PropertyHolder; import org.hibernate.cfg.PropertyHolderBuilder; import org.hibernate.cfg.SecondPass; @@ -69,12 +65,12 @@ public class ListBinder extends CollectionBinder { public void secondPass(Map persistentClasses) throws MappingException { bindStarToManySecondPass( persistentClasses ); - bindIndex( property, getElementType(), buildingContext ); + bindIndex(); } }; } - private void bindIndex(XProperty property, XClass elementType, final MetadataBuildingContext buildingContext) { + private void bindIndex() { final PropertyHolder valueHolder = PropertyHolderBuilder.buildPropertyHolder( collection, qualify( collection.getRole(), "key" ), @@ -93,23 +89,23 @@ public class ListBinder extends CollectionBinder { final BasicValueBinder valueBinder = new BasicValueBinder( BasicValueBinder.Kind.LIST_INDEX, buildingContext ); valueBinder.setColumns( new AnnotatedColumn[] { indexColumn } ); valueBinder.setReturnedClassName( Integer.class.getName() ); - valueBinder.setType( property, elementType, null, null ); + valueBinder.setType( property, getElementType(), null, null ); // valueBinder.setExplicitType( "integer" ); - SimpleValue indexValue = valueBinder.make(); + final SimpleValue indexValue = valueBinder.make(); indexColumn.linkWithValue( indexValue ); listValueMapping.setIndex( indexValue ); listValueMapping.setBaseIndex( indexColumn.getBase() ); if ( listValueMapping.isOneToMany() && !listValueMapping.getKey().isNullable() && !listValueMapping.isInverse() ) { - String entityName = ( (OneToMany) listValueMapping.getElement() ).getReferencedEntityName(); - PersistentClass referenced = buildingContext.getMetadataCollector().getEntityBinding( entityName ); - IndexBackref ib = new IndexBackref(); - ib.setName( '_' + propertyName + "IndexBackref" ); - ib.setUpdateable( false ); - ib.setSelectable( false ); - ib.setCollectionRole( listValueMapping.getRole() ); - ib.setEntityName( listValueMapping.getOwner().getEntityName() ); - ib.setValue( listValueMapping.getIndex() ); - referenced.addProperty( ib ); + final String entityName = ( (OneToMany) listValueMapping.getElement() ).getReferencedEntityName(); + final PersistentClass referenced = buildingContext.getMetadataCollector().getEntityBinding( entityName ); + final IndexBackref backref = new IndexBackref(); + backref.setName( '_' + propertyName + "IndexBackref" ); + backref.setUpdateable( false ); + backref.setSelectable( false ); + backref.setCollectionRole( listValueMapping.getRole() ); + backref.setEntityName( listValueMapping.getOwner().getEntityName() ); + backref.setValue( listValueMapping.getIndex() ); + referenced.addProperty( backref ); } } }