From 0a251e9a0e6206e9a1bc4a6c884faacfaa47d069 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Thu, 27 Jan 2022 17:09:17 +0100 Subject: [PATCH] second round of binder cleanup: CollectionBinder, more AnnotationBinder --- .../model/IdentifierGeneratorDefinition.java | 16 +- .../org/hibernate/cfg/AnnotatedColumn.java | 139 +- .../org/hibernate/cfg/AnnotationBinder.java | 1468 +++++++++-------- .../java/org/hibernate/cfg/BinderHelper.java | 10 +- .../org/hibernate/cfg/ColumnsBuilder.java | 41 +- .../cfg/annotations/CollectionBinder.java | 1175 +++++++------ .../cfg/annotations/IdBagBinder.java | 22 +- .../entitynonentity/EntityNonEntityTest.java | 4 +- .../BasicSessionFactoryScopeTests.java | 2 +- 9 files changed, 1659 insertions(+), 1218 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/IdentifierGeneratorDefinition.java b/hibernate-core/src/main/java/org/hibernate/boot/model/IdentifierGeneratorDefinition.java index 4cc75ee71a..65c67daf67 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/IdentifierGeneratorDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/IdentifierGeneratorDefinition.java @@ -10,6 +10,7 @@ import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.hibernate.internal.util.collections.CollectionHelper; @@ -86,18 +87,9 @@ public class IdentifierGeneratorDefinition implements Serializable { } IdentifierGeneratorDefinition that = (IdentifierGeneratorDefinition) o; - - if ( name != null ? !name.equals( that.name ) : that.name != null ) { - return false; - } - if ( parameters != null ? !parameters.equals( that.parameters ) : that.parameters != null ) { - return false; - } - if ( strategy != null ? !strategy.equals( that.strategy ) : that.strategy != null ) { - return false; - } - - return true; + return Objects.equals(name, that.name) + && Objects.equals(strategy, that.strategy) + && Objects.equals(parameters, that.parameters); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java index 3254fb7c95..e3266fa6b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java @@ -236,7 +236,15 @@ public class AnnotatedColumn { } else { initMappingColumn( - logicalColumnName, propertyName, length, precision, scale, nullable, sqlType, unique, true + logicalColumnName, + propertyName, + length, + precision, + scale, + nullable, + sqlType, + unique, + true ); if ( defaultValue != null ) { mappingColumn.setDefaultValue( defaultValue ); @@ -485,7 +493,7 @@ public class AnnotatedColumn { } return StringHelper.isNotEmpty( explicitTableName ) - && !propertyHolder.getTable().getName().equals( explicitTableName ); + && !propertyHolder.getTable().getName().equals( explicitTableName ); } public Join getJoin() { @@ -518,8 +526,7 @@ public class AnnotatedColumn { mappingColumn.setNullable( false ); } - public static AnnotatedColumn[] buildColumnFromAnnotation( - jakarta.persistence.Column[] anns, + public static AnnotatedColumn[] buildFormulaFromAnnotation( org.hibernate.annotations.Formula formulaAnn, Comment commentAnn, Nullability nullability, @@ -527,8 +534,110 @@ public class AnnotatedColumn { PropertyData inferredData, Map secondaryTables, MetadataBuildingContext context) { - return buildColumnFromAnnotation( - anns, + return buildColumnOrFormulaFromAnnotation( + null, + formulaAnn, + commentAnn, + nullability, + propertyHolder, + inferredData, + secondaryTables, + context + ); + } + + public static AnnotatedColumn[] buildColumnFromNoAnnotation( + Comment commentAnn, + Nullability nullability, + PropertyHolder propertyHolder, + PropertyData inferredData, + Map secondaryTables, + MetadataBuildingContext context) { + return buildColumnsFromAnnotations( + null, + commentAnn, + nullability, + propertyHolder, + inferredData, + secondaryTables, + context + ); + } + + public static AnnotatedColumn[] buildColumnFromAnnotation( + jakarta.persistence.Column column, + Comment commentAnn, + Nullability nullability, + PropertyHolder propertyHolder, + PropertyData inferredData, + Map secondaryTables, + MetadataBuildingContext context) { + return buildColumnOrFormulaFromAnnotation( + column, + null, + commentAnn, + nullability, + propertyHolder, + inferredData, + secondaryTables, + context + ); + } + + public static AnnotatedColumn[] buildColumnsFromAnnotations( + jakarta.persistence.Column[] columns, + Comment commentAnn, + Nullability nullability, + PropertyHolder propertyHolder, + PropertyData inferredData, + Map secondaryTables, + MetadataBuildingContext context) { + return buildColumnsOrFormulaFromAnnotation( + columns, + null, + commentAnn, + nullability, + propertyHolder, + inferredData, + null, + secondaryTables, + context + ); + } + + public static AnnotatedColumn[] buildColumnsFromAnnotations( + jakarta.persistence.Column[] columns, + Comment commentAnn, + Nullability nullability, + PropertyHolder propertyHolder, + PropertyData inferredData, + String suffixForDefaultColumnName, + Map secondaryTables, + MetadataBuildingContext context) { + return buildColumnsOrFormulaFromAnnotation( + columns, + null, + commentAnn, + nullability, + propertyHolder, + inferredData, + suffixForDefaultColumnName, + secondaryTables, + context + ); + } + + public static AnnotatedColumn[] buildColumnOrFormulaFromAnnotation( + jakarta.persistence.Column column, + org.hibernate.annotations.Formula formulaAnn, + Comment commentAnn, + Nullability nullability, + PropertyHolder propertyHolder, + PropertyData inferredData, + Map secondaryTables, + MetadataBuildingContext context) { + return buildColumnsOrFormulaFromAnnotation( + new jakarta.persistence.Column[] { column }, formulaAnn, commentAnn, nullability, @@ -539,8 +648,9 @@ public class AnnotatedColumn { context ); } - public static AnnotatedColumn[] buildColumnFromAnnotation( - jakarta.persistence.Column[] anns, + + public static AnnotatedColumn[] buildColumnsOrFormulaFromAnnotation( + jakarta.persistence.Column[] columnAnns, org.hibernate.annotations.Formula formulaAnn, Comment commentAnn, Nullability nullability, @@ -549,7 +659,7 @@ public class AnnotatedColumn { String suffixForDefaultColumnName, Map secondaryTables, MetadataBuildingContext context) { - AnnotatedColumn[] columns; + if ( formulaAnn != null ) { AnnotatedColumn formulaColumn = new AnnotatedColumn(); formulaColumn.setFormula( formulaAnn.value() ); @@ -557,21 +667,23 @@ public class AnnotatedColumn { formulaColumn.setBuildingContext( context ); formulaColumn.setPropertyHolder( propertyHolder ); formulaColumn.bind(); - columns = new AnnotatedColumn[] { formulaColumn }; + return new AnnotatedColumn[] { formulaColumn }; } else { - jakarta.persistence.Column[] actualCols = anns; + jakarta.persistence.Column[] actualCols = columnAnns; jakarta.persistence.Column[] overriddenCols = propertyHolder.getOverriddenColumn( StringHelper.qualify( propertyHolder.getPath(), inferredData.getPropertyName() ) ); if ( overriddenCols != null ) { //check for overridden first - if ( anns != null && overriddenCols.length != anns.length ) { + if ( columnAnns != null && overriddenCols.length != columnAnns.length ) { throw new AnnotationException( "AttributeOverride.column() should override all columns for now" ); } actualCols = overriddenCols.length == 0 ? null : overriddenCols; LOG.debugf( "Column(s) overridden for property %s", inferredData.getPropertyName() ); } + + AnnotatedColumn[] columns; if ( actualCols == null ) { columns = buildImplicitColumn( inferredData, @@ -667,8 +779,9 @@ public class AnnotatedColumn { columns[index] = column; } } + + return columns; } - return columns; } private void applyColumnDefault(PropertyData inferredData, int length) { 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 5999ffc7de..2d99b3fbe0 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -203,10 +203,21 @@ import jakarta.persistence.UniqueConstraint; import jakarta.persistence.Version; import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromAnnotation; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromNoAnnotation; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnsFromAnnotations; +import static org.hibernate.cfg.AnnotatedColumn.buildFormulaFromAnnotation; +import static org.hibernate.cfg.AnnotatedDiscriminatorColumn.buildDiscriminatorColumn; import static org.hibernate.cfg.AnnotatedJoinColumn.buildJoinColumnsWithDefaultColumnSuffix; +import static org.hibernate.cfg.AnnotatedJoinColumn.buildJoinTableJoinColumns; import static org.hibernate.cfg.BinderHelper.getMappedSuperclassOrNull; +import static org.hibernate.cfg.BinderHelper.getPropertyOverriddenByMapperOrMapsId; +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.cfg.annotations.CollectionBinder.getCollectionBinder; import static org.hibernate.internal.CoreLogging.messageLogger; +import static org.hibernate.internal.util.StringHelper.nullIfEmpty; import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; /** @@ -1120,7 +1131,7 @@ public final class AnnotationBinder { DiscriminatorFormula discFormulaAnn = getOverridableAnnotation( clazzToProcess, DiscriminatorFormula.class, context ); if ( isRoot ) { - discriminatorColumn = AnnotatedDiscriminatorColumn.buildDiscriminatorColumn( + discriminatorColumn = buildDiscriminatorColumn( discriminatorType, discAnn, discFormulaAnn, @@ -1176,35 +1187,32 @@ public final class AnnotationBinder { // 2) There is not an explicit DiscriminatorColumn annotation && we are told to create them implicitly final boolean generateDiscriminatorColumn; if ( discriminatorColumnAnnotation != null ) { - if ( context.getBuildingOptions().ignoreExplicitDiscriminatorsForJoinedInheritance() ) { - LOG.debugf( "Ignoring explicit DiscriminatorColumn annotation on: %s", clazzToProcess.getName() ); - generateDiscriminatorColumn = false; - } - else { + generateDiscriminatorColumn = !context.getBuildingOptions().ignoreExplicitDiscriminatorsForJoinedInheritance(); + if (generateDiscriminatorColumn) { LOG.applyingExplicitDiscriminatorColumnForJoined( clazzToProcess.getName(), AvailableSettings.IGNORE_EXPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS ); - generateDiscriminatorColumn = true; + } + else { + LOG.debugf( "Ignoring explicit DiscriminatorColumn annotation on: %s", clazzToProcess.getName() ); } } else { - if ( context.getBuildingOptions().createImplicitDiscriminatorsForJoinedInheritance() ) { + generateDiscriminatorColumn = context.getBuildingOptions().createImplicitDiscriminatorsForJoinedInheritance(); + if ( generateDiscriminatorColumn ) { LOG.debug( "Applying implicit DiscriminatorColumn using DiscriminatorColumn defaults" ); - generateDiscriminatorColumn = true; } else { LOG.debug( "Ignoring implicit (absent) DiscriminatorColumn" ); - generateDiscriminatorColumn = false; } } if ( generateDiscriminatorColumn ) { - final DiscriminatorType discriminatorType = discriminatorColumnAnnotation != null - ? discriminatorColumnAnnotation.discriminatorType() - : DiscriminatorType.STRING; - return AnnotatedDiscriminatorColumn.buildDiscriminatorColumn( - discriminatorType, + return buildDiscriminatorColumn( + discriminatorColumnAnnotation != null + ? discriminatorColumnAnnotation.discriminatorType() + : DiscriminatorType.STRING, discriminatorColumnAnnotation, null, context @@ -1404,9 +1412,7 @@ public final class AnnotationBinder { if ( associatedClassWithIdClass == null ) { //we cannot know for sure here unless we try and find the @EmbeddedId //Let's not do this thorough checking but do some extra validation - final XProperty property = idPropertyOnBaseClass.getProperty(); - return property.isAnnotationPresent( ManyToOne.class ) - || property.isAnnotationPresent( OneToOne.class ); + return hasToOneAnnotation( idPropertyOnBaseClass.getProperty() ); } else { @@ -1500,7 +1506,7 @@ public final class AnnotationBinder { Map inheritanceStatePerClass, MetadataBuildingContext context, InheritanceState inheritanceState) { - InheritanceState superEntityState = InheritanceState.getInheritanceStateOfSuperEntity( + InheritanceState superEntityState = getInheritanceStateOfSuperEntity( clazzToProcess, inheritanceStatePerClass ); PersistentClass superEntity = superEntityState != null @@ -1510,7 +1516,7 @@ public final class AnnotationBinder { //check if superclass is not a potential persistent class if ( inheritanceState.hasParents() ) { throw new AssertionFailure( - "Subclass has to be binded after it's mother class: " + "Subclass has to be bound after its parent class: " + superEntityState.getClazz().getName() ); } @@ -1519,8 +1525,8 @@ public final class AnnotationBinder { } /** - * Process the filters defined on the given class, as well as all filters defined - * on the MappedSuperclass(s) in the inheritance hierarchy + * Process the filters defined on the given class, as well as all filters + * defined on the MappedSuperclass(es) in the inheritance hierarchy */ private static void bindFiltersAndFilterDefs( XClass annotatedClass, @@ -1820,7 +1826,7 @@ public final class AnnotationBinder { * since it has to be parsed before any association by Hibernate */ final XAnnotatedElement element = propertyAnnotatedElement.getProperty(); - if ( element.isAnnotationPresent( Id.class ) || element.isAnnotationPresent( EmbeddedId.class ) ) { + if ( hasIdAnnotation(element) ) { inFlightPropertyDataList.add( 0, propertyAnnotatedElement ); /* * The property must be put in hibernate.properties as it's a system wide property. Fixable? @@ -1873,7 +1879,7 @@ public final class AnnotationBinder { } } - if ( element.isAnnotationPresent( ManyToOne.class ) || element.isAnnotationPresent( OneToOne.class ) ) { + if ( hasToOneAnnotation(element) ) { context.getMetadataCollector().addToOneAndIdProperty( entity, propertyAnnotatedElement ); } idPropertyCounter++; @@ -1888,6 +1894,11 @@ public final class AnnotationBinder { return idPropertyCounter; } + private static boolean hasIdAnnotation(XAnnotatedElement element) { + return element.isAnnotationPresent(Id.class) + || element.isAnnotationPresent(EmbeddedId.class); + } + /* * Process annotation of a particular property */ @@ -1921,8 +1932,7 @@ public final class AnnotationBinder { * ordering does not matter */ - final boolean traceEnabled = LOG.isTraceEnabled(); - if ( traceEnabled ) { + if ( LOG.isTraceEnabled() ) { LOG.tracev( "Processing annotations of {0}.{1}" , propertyHolder.getEntityName(), inferredData.getPropertyName() ); } @@ -1970,9 +1980,7 @@ public final class AnnotationBinder { propertyBinder.setEntityBinder( entityBinder ); propertyBinder.setInheritanceStatePerClass( inheritanceStatePerClass ); - boolean isId = !entityBinder.isIgnoreIdAnnotations() && - ( property.isAnnotationPresent( Id.class ) - || property.isAnnotationPresent( EmbeddedId.class ) ); + boolean isId = !entityBinder.isIgnoreIdAnnotations() && hasIdAnnotation( property ); propertyBinder.setId( isId ); final LazyGroup lazyGroupAnnotation = property.getAnnotation( LazyGroup.class ); @@ -1981,584 +1989,94 @@ public final class AnnotationBinder { } if ( property.isAnnotationPresent( Version.class ) ) { - if ( isIdentifierMapper ) { - throw new AnnotationException( - "@IdClass class should not have @Version property" - ); - } - if ( !( propertyHolder.getPersistentClass() instanceof RootClass ) ) { - throw new AnnotationException( - "Unable to define/override @Version on a subclass: " - + propertyHolder.getEntityName() - ); - } - if ( !propertyHolder.isEntity() ) { - throw new AnnotationException( - "Unable to define @Version on an embedded class: " - + propertyHolder.getEntityName() - ); - } - if ( traceEnabled ) { - LOG.tracev( "{0} is a version property", inferredData.getPropertyName() ); - } - RootClass rootClass = ( RootClass ) propertyHolder.getPersistentClass(); - propertyBinder.setColumns( columns ); - Property prop = propertyBinder.makePropertyValueAndBind(); - setVersionInformation( property, propertyBinder ); - rootClass.setVersion( prop ); - - //If version is on a mapped superclass, update the mapping - final org.hibernate.mapping.MappedSuperclass superclass = getMappedSuperclassOrNull( - inferredData.getDeclaringClass(), + bindVersionProperty( + propertyHolder, + inferredData, + isIdentifierMapper, + context, inheritanceStatePerClass, - context + property, + columns, + propertyBinder ); - if ( superclass != null ) { - superclass.setDeclaredVersion( prop ); - } - else { - //we know the property is on the actual entity - rootClass.setDeclaredVersion( prop ); - } - - SimpleValue simpleValue = ( SimpleValue ) prop.getValue(); - simpleValue.setNullValue( "undefined" ); - rootClass.setOptimisticLockStyle( OptimisticLockStyle.VERSION ); - if ( traceEnabled ) { - LOG.tracev( "Version name: {0}, unsavedValue: {1}", rootClass.getVersion().getName(), - ( (SimpleValue) rootClass.getVersion().getValue() ).getNullValue() ); - } } else { - final boolean forcePersist = property.isAnnotationPresent( MapsId.class ) - || property.isAnnotationPresent( Id.class ); if ( property.isAnnotationPresent( ManyToOne.class ) ) { - ManyToOne ann = property.getAnnotation( ManyToOne.class ); - - //check validity - if ( property.isAnnotationPresent( Column.class ) - || property.isAnnotationPresent( Columns.class ) ) { - throw new AnnotationException( - "@Column(s) not allowed on a @ManyToOne property: " - + BinderHelper.getPath( propertyHolder, inferredData ) - ); - } - - Cascade hibernateCascade = property.getAnnotation( Cascade.class ); - NotFound notFound = property.getAnnotation( NotFound.class ); - boolean ignoreNotFound = notFound != null && notFound.action().equals( NotFoundAction.IGNORE ); - matchIgnoreNotFoundWithFetchType(propertyHolder.getEntityName(), property.getName(), ignoreNotFound, ann.fetch()); - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - boolean onDeleteCascade = onDeleteAnn != null && OnDeleteAction.CASCADE.equals( onDeleteAnn.action() ); - 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 ) && !ignoreNotFound; bindManyToOne( - getCascadeStrategy( ann.cascade(), hibernateCascade, false, forcePersist ), - joinColumns, - !mandatory, - ignoreNotFound, - onDeleteCascade, - ToOneBinder.getTargetEntity( inferredData, context ), propertyHolder, inferredData, - false, isIdentifierMapper, inSecondPass, + context, + property, + joinColumns, propertyBinder, - context + isForcePersist(property) ); } else if ( property.isAnnotationPresent( OneToOne.class ) ) { - OneToOne ann = property.getAnnotation( OneToOne.class ); - - //check validity - if ( property.isAnnotationPresent( Column.class ) - || property.isAnnotationPresent( Columns.class ) ) { - throw new AnnotationException( - "@Column(s) not allowed on a @OneToOne property: " - + BinderHelper.getPath( propertyHolder, inferredData ) - ); - } - - //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 ); - boolean ignoreNotFound = notFound != null && notFound.action().equals( NotFoundAction.IGNORE ); - // 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 ) && !ignoreNotFound ); - matchIgnoreNotFoundWithFetchType(propertyHolder.getEntityName(), property.getName(), ignoreNotFound, ann.fetch()); - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - boolean onDeleteCascade = onDeleteAnn != null && OnDeleteAction.CASCADE.equals( onDeleteAnn.action() ); - JoinTable assocTable = propertyHolder.getJoinTable( property ); - if ( assocTable != null ) { - Join join = propertyHolder.addJoin( assocTable, false ); - for ( AnnotatedJoinColumn joinColumn : joinColumns ) { - joinColumn.setExplicitTableName( join.getTable().getName() ); - } - } bindOneToOne( - getCascadeStrategy( ann.cascade(), hibernateCascade, ann.orphanRemoval(), forcePersist ), - joinColumns, - !mandatory, - getFetchMode( ann.fetch() ), - ignoreNotFound, onDeleteCascade, - ToOneBinder.getTargetEntity( inferredData, context ), propertyHolder, inferredData, - ann.mappedBy(), - trueOneToOne, isIdentifierMapper, inSecondPass, + context, + property, + joinColumns, propertyBinder, - context + isForcePersist(property) ); } else if ( property.isAnnotationPresent( org.hibernate.annotations.Any.class ) ) { - - //check validity - if ( property.isAnnotationPresent( Columns.class ) ) { - throw new AnnotationException( - String.format( - Locale.ROOT, - "@Columns not allowed on a @Any property [%s]; @Column or @Formula is used to map the discriminator" + - "and only one is allowed", - BinderHelper.getPath( propertyHolder, inferredData ) - ) - ); - } - - Cascade hibernateCascade = property.getAnnotation( Cascade.class ); - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - boolean onDeleteCascade = onDeleteAnn != null && OnDeleteAction.CASCADE.equals( onDeleteAnn.action() ); - JoinTable assocTable = propertyHolder.getJoinTable( property ); - if ( assocTable != null ) { - Join join = propertyHolder.addJoin( assocTable, false ); - for ( AnnotatedJoinColumn joinColumn : joinColumns ) { - joinColumn.setExplicitTableName( join.getTable().getName() ); - } - } bindAny( - getCascadeStrategy( null, hibernateCascade, false, forcePersist ), - //@Any has not cascade attribute - joinColumns, - onDeleteCascade, - nullability, propertyHolder, + nullability, inferredData, entityBinder, isIdentifierMapper, - context + context, + property, + joinColumns, + isForcePersist(property) ); } else if ( property.isAnnotationPresent( OneToMany.class ) || property.isAnnotationPresent( ManyToMany.class ) || property.isAnnotationPresent( ElementCollection.class ) || property.isAnnotationPresent( ManyToAny.class ) ) { - OneToMany oneToManyAnn = property.getAnnotation( OneToMany.class ); - ManyToMany manyToManyAnn = property.getAnnotation( ManyToMany.class ); - ElementCollection elementCollectionAnn = property.getAnnotation( ElementCollection.class ); - - if ( ( oneToManyAnn != null || manyToManyAnn != null || elementCollectionAnn != null ) && - isToManyAssociationWithinEmbeddableCollection( - propertyHolder ) ) { - throw new AnnotationException( - "@OneToMany, @ManyToMany or @ElementCollection cannot be used inside an @Embeddable that is also contained within an @ElementCollection: " - + BinderHelper.getPath( - propertyHolder, - inferredData - ) - ); - } - - final OrderColumn orderColumnAnnotation = property.getAnnotation( OrderColumn.class ); - final org.hibernate.annotations.IndexColumn indexColumnAnnotation = - property.getAnnotation( org.hibernate.annotations.IndexColumn.class ); - final ListIndexBase indexBaseAnnotation = property.getAnnotation( ListIndexBase.class ); - - final IndexColumn indexColumn = IndexColumn.fromAnnotations( - orderColumnAnnotation, - indexColumnAnnotation, - indexBaseAnnotation, - propertyHolder, - inferredData, - entityBinder.getSecondaryTables(), - context - ); - - CollectionBinder collectionBinder = CollectionBinder.getCollectionBinder( - property, - // ugh - property.isAnnotationPresent( MapKeyJavaType.class ) - || property.isAnnotationPresent( MapKeyJdbcType.class ) - || property.isAnnotationPresent( MapKeyJdbcTypeCode.class ) - || property.isAnnotationPresent( MapKeyMutability.class ) - || property.isAnnotationPresent( MapKey.class ) - || property.isAnnotationPresent( MapKeyCustomType.class ), - context - ); - collectionBinder.setIndexColumn( indexColumn ); - collectionBinder.setMapKey( property.getAnnotation( MapKey.class ) ); - collectionBinder.setPropertyName( inferredData.getPropertyName() ); - - collectionBinder.setBatchSize( property.getAnnotation( BatchSize.class ) ); - - collectionBinder.setJpaOrderBy( property.getAnnotation( jakarta.persistence.OrderBy.class ) ); - collectionBinder.setSqlOrderBy( getOverridableAnnotation( property, OrderBy.class, context ) ); - - collectionBinder.setNaturalSort( property.getAnnotation( SortNatural.class ) ); - collectionBinder.setComparatorSort( property.getAnnotation( SortComparator.class ) ); - - Cache cachAnn = property.getAnnotation( Cache.class ); - collectionBinder.setCache( cachAnn ); - collectionBinder.setPropertyHolder( propertyHolder ); - Cascade hibernateCascade = property.getAnnotation( Cascade.class ); - NotFound notFound = property.getAnnotation( NotFound.class ); - boolean ignoreNotFound = notFound != null && notFound.action().equals( NotFoundAction.IGNORE ); - collectionBinder.setIgnoreNotFound( ignoreNotFound ); - collectionBinder.setCollectionType( inferredData.getProperty().getElementClass() ); - collectionBinder.setAccessType( inferredData.getDefaultAccess() ); - - AnnotatedColumn[] elementColumns; - //do not use "element" if you are a JPA 2 @ElementCollection only for legacy Hibernate mappings - boolean isJPA2ForValueMapping = property.isAnnotationPresent( ElementCollection.class ); - PropertyData virtualProperty = isJPA2ForValueMapping ? inferredData : new WrappedInferredData( - inferredData, "element" - ); - Comment comment = property.getAnnotation(Comment.class); - if ( property.isAnnotationPresent( Column.class ) - || property.isAnnotationPresent( Formula.class ) ) { - Column ann = property.getAnnotation( Column.class ); - Formula formulaAnn = getOverridableAnnotation( property, Formula.class, context ); - elementColumns = buildColumnFromAnnotation( - new Column[] { ann }, - formulaAnn, - comment, - nullability, - propertyHolder, - virtualProperty, - entityBinder.getSecondaryTables(), - context - ); - } - else if ( property.isAnnotationPresent( Columns.class ) ) { - Columns anns = property.getAnnotation( Columns.class ); - elementColumns = buildColumnFromAnnotation( - anns.columns(), - null, - comment, - nullability, - propertyHolder, - virtualProperty, - entityBinder.getSecondaryTables(), - context - ); - } - else { - elementColumns = buildColumnFromAnnotation( - null, - null, - comment, - nullability, - propertyHolder, - virtualProperty, - entityBinder.getSecondaryTables(), - context - ); - } - - JoinColumn[] joinKeyColumns = mapKeyColumns( + bindCollection( propertyHolder, + nullability, inferredData, + classGenerators, entityBinder, + isIdentifierMapper, context, + inheritanceStatePerClass, property, - collectionBinder, - comment + joinColumns ); - AnnotatedJoinColumn[] mapJoinColumns = buildJoinColumnsWithDefaultColumnSuffix( - joinKeyColumns, - comment, - null, - entityBinder.getSecondaryTables(), - propertyHolder, - inferredData.getPropertyName(), - "_KEY", - context - ); - collectionBinder.setMapKeyManyToManyColumns( mapJoinColumns ); - - //potential element - collectionBinder.setEmbedded( property.isAnnotationPresent( Embedded.class ) ); - collectionBinder.setElementColumns( elementColumns ); - collectionBinder.setProperty( property ); - - //TODO enhance exception with @ManyToAny and @CollectionOfElements - if ( oneToManyAnn != null && manyToManyAnn != null ) { - throw new AnnotationException( - "@OneToMany and @ManyToMany on the same property is not allowed: " - + propertyHolder.getEntityName() + "." + inferredData.getPropertyName() - ); - } - String mappedBy = null; - if ( oneToManyAnn != null ) { - for ( AnnotatedJoinColumn column : joinColumns ) { - if ( column.isSecondary() ) { - throw new NotYetImplementedException( "Collections having FK in secondary table" ); - } - } - collectionBinder.setFkJoinColumns( joinColumns ); - mappedBy = oneToManyAnn.mappedBy(); - //noinspection unchecked - collectionBinder.setTargetEntity( - context.getBootstrapContext().getReflectionManager().toXClass( oneToManyAnn.targetEntity() ) - ); - collectionBinder.setCascadeStrategy( - getCascadeStrategy( - oneToManyAnn.cascade(), hibernateCascade, oneToManyAnn.orphanRemoval(), false - ) - ); - collectionBinder.setOneToMany( true ); - } - else if ( elementCollectionAnn != null ) { - for ( AnnotatedJoinColumn column : joinColumns ) { - if ( column.isSecondary() ) { - throw new NotYetImplementedException( "Collections having FK in secondary table" ); - } - } - collectionBinder.setFkJoinColumns( joinColumns ); - mappedBy = ""; - final Class targetElement = elementCollectionAnn.targetClass(); - collectionBinder.setTargetEntity( - context.getBootstrapContext().getReflectionManager().toXClass( targetElement ) - ); - //collectionBinder.setCascadeStrategy( getCascadeStrategy( embeddedCollectionAnn.cascade(), hibernateCascade ) ); - collectionBinder.setOneToMany( true ); - } - else if ( manyToManyAnn != null ) { - mappedBy = manyToManyAnn.mappedBy(); - //noinspection unchecked - collectionBinder.setTargetEntity( - context.getBootstrapContext().getReflectionManager().toXClass( manyToManyAnn.targetEntity() ) - ); - collectionBinder.setCascadeStrategy( - getCascadeStrategy( - manyToManyAnn.cascade(), hibernateCascade, false, false - ) - ); - collectionBinder.setOneToMany( false ); - } - else if ( property.isAnnotationPresent( ManyToAny.class ) ) { - mappedBy = ""; - collectionBinder.setTargetEntity( - context.getBootstrapContext().getReflectionManager().toXClass( void.class ) - ); - collectionBinder.setCascadeStrategy( getCascadeStrategy( null, hibernateCascade, false, false ) ); - collectionBinder.setOneToMany( false ); - } - collectionBinder.setMappedBy( mappedBy ); - - bindJoinedTableAssociation( - property, - context, - entityBinder, - collectionBinder, - propertyHolder, - inferredData, - mappedBy - ); - - OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); - boolean onDeleteCascade = onDeleteAnn != null && OnDeleteAction.CASCADE.equals( onDeleteAnn.action() ); - collectionBinder.setCascadeDeleteEnabled( onDeleteCascade ); - if ( isIdentifierMapper ) { - collectionBinder.setInsertable( false ); - collectionBinder.setUpdatable( false ); - } - if ( property.isAnnotationPresent( CollectionId.class ) ) { //do not compute the generators unless necessary - HashMap localGenerators = new HashMap<>( classGenerators ); - localGenerators.putAll( buildGenerators( property, context ) ); - collectionBinder.setLocalGenerators( localGenerators ); - - } - collectionBinder.setInheritanceStatePerClass( inheritanceStatePerClass ); - collectionBinder.setDeclaringClass( inferredData.getDeclaringClass() ); - collectionBinder.bind(); - } //Either a regular property or a basic @Id or @EmbeddedId while not ignoring id annotations else if ( !isId || !entityBinder.isIgnoreIdAnnotations() ) { - //define whether the type is a component or not - - boolean isComponent = false; - - //Overrides from @MapsId if needed - boolean isOverridden = false; - if ( isId || propertyHolder.isOrWithinEmbeddedId() || propertyHolder.isInIdClass() ) { - //the associated entity could be using an @IdClass making the overridden property a component - final PropertyData overridingProperty = BinderHelper.getPropertyOverriddenByMapperOrMapsId( - isId, - propertyHolder, - property.getName(), - context - ); - if ( overridingProperty != null ) { - isOverridden = true; - final InheritanceState state = inheritanceStatePerClass.get( overridingProperty.getClassOrElement() ); - if ( state != null ) { - isComponent = state.hasIdClassOrEmbeddedId(); - } - //Get the new column - columns = columnsBuilder.overrideColumnFromMapperOrMapsIdProperty( isId ); - } - } - - isComponent = isComponent - || property.isAnnotationPresent( Embedded.class ) - || property.isAnnotationPresent( EmbeddedId.class ) - || returnedClass.isAnnotationPresent( Embeddable.class ); - - - if ( isComponent ) { - String referencedEntityName = null; - if ( isOverridden ) { - final PropertyData mapsIdProperty = BinderHelper.getPropertyOverriddenByMapperOrMapsId( - isId, propertyHolder, property.getName(), context - ); - referencedEntityName = mapsIdProperty.getClassOrElementName(); - } - - final AccessType propertyAccessor = entityBinder.getPropertyAccessor( property ); - final Class customInstantiatorImpl = determineCustomInstantiator( property, returnedClass, context ); - - propertyBinder = bindComponent( - inferredData, - propertyHolder, - propertyAccessor, - entityBinder, - isIdentifierMapper, - context, - isComponentEmbedded, - isId, - inheritanceStatePerClass, - referencedEntityName, - customInstantiatorImpl, - isOverridden ? ( AnnotatedJoinColumn[] ) columns : null - ); - } - else { - //provide the basic property mapping - boolean optional = true; - boolean lazy = false; - if ( property.isAnnotationPresent( Basic.class ) ) { - Basic ann = property.getAnnotation( Basic.class ); - optional = ann.optional(); - lazy = ann.fetch() == FetchType.LAZY; - } - //implicit type will check basic types and Serializable classes - if ( isId || ( !optional && nullability != Nullability.FORCED_NULL ) ) { - //force columns to not null - for ( AnnotatedColumn col : columns ) { - if ( isId && col.isFormula() ) { - throw new CannotForceNonNullableException( - String.format( - Locale.ROOT, - "Identifier property [%s] cannot contain formula mapping [%s]", - HCANNHelper.annotatedElementSignature( property ), - col.getFormulaString() - ) - ); - } - col.forceNotNull(); - } - } - - propertyBinder.setLazy( lazy ); - propertyBinder.setColumns( columns ); - if ( isOverridden ) { - final PropertyData mapsIdProperty = BinderHelper.getPropertyOverriddenByMapperOrMapsId( - isId, propertyHolder, property.getName(), context - ); - propertyBinder.setReferencedEntityName( mapsIdProperty.getClassOrElementName() ); - } - - propertyBinder.makePropertyValueAndBind(); - - } - if ( isOverridden ) { - final PropertyData mapsIdProperty = BinderHelper.getPropertyOverriddenByMapperOrMapsId( - isId, propertyHolder, property.getName(), context - ); - final IdentifierGeneratorDefinition.Builder foreignGeneratorBuilder = new IdentifierGeneratorDefinition.Builder(); - foreignGeneratorBuilder.setName( "Hibernate-local--foreign generator" ); - foreignGeneratorBuilder.setStrategy( "foreign" ); - foreignGeneratorBuilder.addParam( "property", mapsIdProperty.getPropertyName() ); - final IdentifierGeneratorDefinition foreignGenerator = foreignGeneratorBuilder.build(); - - if ( isGlobalGeneratorNameGlobal( context ) ) { - SecondPass secondPass = new IdGeneratorResolverSecondPass( - (SimpleValue) propertyBinder.getValue(), - property, - foreignGenerator.getStrategy(), - foreignGenerator.getName(), - context, - foreignGenerator - ); - context.getMetadataCollector().addSecondPass( secondPass ); - } - else { - Map localGenerators = new HashMap<>(classGenerators); - localGenerators.put( foreignGenerator.getName(), foreignGenerator ); - - BinderHelper.makeIdGenerator( - (SimpleValue) propertyBinder.getValue(), - property, - foreignGenerator.getStrategy(), - foreignGenerator.getName(), - context, - localGenerators - ); - } - } - if ( isId ) { - //components and regular basic types create SimpleValue objects - final SimpleValue value = ( SimpleValue ) propertyBinder.getValue(); - if ( !isOverridden ) { - processId( - propertyHolder, - inferredData, - value, - classGenerators, - isIdentifierMapper, - context - ); - } - } + columns = bindBasic( + propertyHolder, + nullability, + inferredData, + classGenerators, + entityBinder, + isIdentifierMapper, + isComponentEmbedded, + context, + inheritanceStatePerClass, + property, + columnsBuilder, + columns, + returnedClass, + propertyBinder, + isId + ); } } @@ -2567,6 +2085,618 @@ public final class AnnotationBinder { addNaturalIds(inSecondPass, property, columns, joinColumns); } + private static boolean isForcePersist(XProperty property) { + return property.isAnnotationPresent(MapsId.class) + || property.isAnnotationPresent(Id.class); + } + + private static void bindVersionProperty( + PropertyHolder propertyHolder, + PropertyData inferredData, + boolean isIdentifierMapper, + MetadataBuildingContext context, + Map inheritanceStatePerClass, + XProperty property, + AnnotatedColumn[] columns, + PropertyBinder propertyBinder) { + if (isIdentifierMapper) { + throw new AnnotationException( + "@IdClass class should not have @Version property" + ); + } + if ( !( propertyHolder.getPersistentClass() instanceof RootClass ) ) { + throw new AnnotationException( + "Unable to define/override @Version on a subclass: " + + propertyHolder.getEntityName() + ); + } + if ( !propertyHolder.isEntity() ) { + throw new AnnotationException( + "Unable to define @Version on an embedded class: " + + propertyHolder.getEntityName() + ); + } + if ( LOG.isTraceEnabled() ) { + LOG.tracev( "{0} is a version property", inferredData.getPropertyName() ); + } + RootClass rootClass = (RootClass) propertyHolder.getPersistentClass(); + propertyBinder.setColumns(columns); + Property prop = propertyBinder.makePropertyValueAndBind(); + setVersionInformation(property, propertyBinder); + rootClass.setVersion( prop ); + + //If version is on a mapped superclass, update the mapping + final org.hibernate.mapping.MappedSuperclass superclass = getMappedSuperclassOrNull( + inferredData.getDeclaringClass(), + inheritanceStatePerClass, + context + ); + if ( superclass != null ) { + superclass.setDeclaredVersion( prop ); + } + else { + //we know the property is on the actual entity + rootClass.setDeclaredVersion( prop ); + } + + ( (SimpleValue) prop.getValue() ).setNullValue( "undefined" ); + rootClass.setOptimisticLockStyle( OptimisticLockStyle.VERSION ); + if ( LOG.isTraceEnabled() ) { + LOG.tracev( "Version name: {0}, unsavedValue: {1}", rootClass.getVersion().getName(), + ( (SimpleValue) rootClass.getVersion().getValue() ).getNullValue() ); + } + } + + private static AnnotatedColumn[] bindBasic( + PropertyHolder propertyHolder, + Nullability nullability, + PropertyData inferredData, + Map classGenerators, + EntityBinder entityBinder, + boolean isIdentifierMapper, + boolean isComponentEmbedded, + MetadataBuildingContext context, + Map inheritanceStatePerClass, + XProperty property, + ColumnsBuilder columnsBuilder, + AnnotatedColumn[] columns, XClass returnedClass, + PropertyBinder propertyBinder, + boolean isId) { + //define whether the type is a component or not + + boolean isComponent = false; + + //Overrides from @MapsId if needed + boolean isOverridden = false; + if ( isId || propertyHolder.isOrWithinEmbeddedId() || propertyHolder.isInIdClass() ) { + //the associated entity could be using an @IdClass making the overridden property a component + PropertyData overridingProperty = getPropertyOverriddenByMapperOrMapsId( + isId, + propertyHolder, + property.getName(), + context + ); + if ( overridingProperty != null ) { + isOverridden = true; + final InheritanceState state = inheritanceStatePerClass.get( overridingProperty.getClassOrElement() ); + if ( state != null ) { + isComponent = state.hasIdClassOrEmbeddedId(); + } + //Get the new column + columns = columnsBuilder.overrideColumnFromMapperOrMapsIdProperty(isId); + } + } + + isComponent = isComponent + || property.isAnnotationPresent( Embedded.class ) + || property.isAnnotationPresent( EmbeddedId.class ) + || returnedClass.isAnnotationPresent( Embeddable.class ); + + + if ( isComponent ) { + String referencedEntityName = null; + if ( isOverridden ) { + PropertyData mapsIdProperty = getPropertyOverriddenByMapperOrMapsId( + isId, propertyHolder, property.getName(), context + ); + referencedEntityName = mapsIdProperty.getClassOrElementName(); + } + + propertyBinder = bindComponent( + inferredData, + propertyHolder, + entityBinder.getPropertyAccessor(property), + entityBinder, + isIdentifierMapper, + context, + isComponentEmbedded, + isId, + inheritanceStatePerClass, + referencedEntityName, + determineCustomInstantiator(property, returnedClass, context), + isOverridden ? ( AnnotatedJoinColumn[] ) columns : null + ); + } + else { + //provide the basic property mapping + boolean optional = true; + boolean lazy = false; + if ( property.isAnnotationPresent( Basic.class ) ) { + Basic ann = property.getAnnotation( Basic.class ); + optional = ann.optional(); + lazy = ann.fetch() == FetchType.LAZY; + } + //implicit type will check basic types and Serializable classes + if ( isId || !optional && nullability != Nullability.FORCED_NULL ) { + //force columns to not null + for ( AnnotatedColumn col : columns) { + if ( isId && col.isFormula() ) { + throw new CannotForceNonNullableException( + String.format( + Locale.ROOT, + "Identifier property [%s] cannot contain formula mapping [%s]", + HCANNHelper.annotatedElementSignature(property), + col.getFormulaString() + ) + ); + } + col.forceNotNull(); + } + } + + propertyBinder.setLazy( lazy ); + propertyBinder.setColumns(columns); + if ( isOverridden ) { + final PropertyData mapsIdProperty = getPropertyOverriddenByMapperOrMapsId( + isId, propertyHolder, property.getName(), context + ); + propertyBinder.setReferencedEntityName( mapsIdProperty.getClassOrElementName() ); + } + + propertyBinder.makePropertyValueAndBind(); + + } + if ( isOverridden ) { + final PropertyData mapsIdProperty = getPropertyOverriddenByMapperOrMapsId( + isId, propertyHolder, property.getName(), context + ); + final IdentifierGeneratorDefinition.Builder foreignGeneratorBuilder = + new IdentifierGeneratorDefinition.Builder(); + foreignGeneratorBuilder.setName( "Hibernate-local--foreign generator" ); + foreignGeneratorBuilder.setStrategy( "foreign" ); + foreignGeneratorBuilder.addParam( "property", mapsIdProperty.getPropertyName() ); + final IdentifierGeneratorDefinition foreignGenerator = foreignGeneratorBuilder.build(); + + if ( isGlobalGeneratorNameGlobal(context) ) { + SecondPass secondPass = new IdGeneratorResolverSecondPass( + (SimpleValue) propertyBinder.getValue(), + property, + foreignGenerator.getStrategy(), + foreignGenerator.getName(), + context, + foreignGenerator + ); + context.getMetadataCollector().addSecondPass( secondPass ); + } + else { + Map localGenerators = new HashMap<>(classGenerators); + localGenerators.put( foreignGenerator.getName(), foreignGenerator ); + + makeIdGenerator( + (SimpleValue) propertyBinder.getValue(), + property, + foreignGenerator.getStrategy(), + foreignGenerator.getName(), + context, + localGenerators + ); + } + } + if (isId) { + //components and regular basic types create SimpleValue objects + if ( !isOverridden ) { + processId( + propertyHolder, + inferredData, + (SimpleValue) propertyBinder.getValue(), + classGenerators, + isIdentifierMapper, + context + ); + } + } + return columns; + } + + private static void bindCollection( + PropertyHolder propertyHolder, + Nullability nullability, + PropertyData inferredData, + Map classGenerators, + EntityBinder entityBinder, + boolean isIdentifierMapper, + MetadataBuildingContext context, + Map inheritanceStatePerClass, + XProperty property, + AnnotatedJoinColumn[] joinColumns) { + + OneToMany oneToManyAnn = property.getAnnotation( OneToMany.class ); + ManyToMany manyToManyAnn = property.getAnnotation( ManyToMany.class ); + ElementCollection elementCollectionAnn = property.getAnnotation( ElementCollection.class ); + + if ( ( oneToManyAnn != null || manyToManyAnn != null || elementCollectionAnn != null ) + && isToManyAssociationWithinEmbeddableCollection(propertyHolder) ) { + throw new AnnotationException( + "@OneToMany, @ManyToMany or @ElementCollection cannot be used inside an @Embeddable that is also contained within an @ElementCollection: " + + BinderHelper.getPath( + propertyHolder, + inferredData + ) + ); + } + + final IndexColumn indexColumn = IndexColumn.fromAnnotations( + property.getAnnotation( OrderColumn.class ), + property.getAnnotation( org.hibernate.annotations.IndexColumn.class ), + property.getAnnotation( ListIndexBase.class ), + propertyHolder, + inferredData, + entityBinder.getSecondaryTables(), + context + ); + + CollectionBinder collectionBinder = getCollectionBinder( property, hasMapKeyAnnotation( property ), context ); + collectionBinder.setIndexColumn( indexColumn ); + collectionBinder.setMapKey( property.getAnnotation( MapKey.class ) ); + collectionBinder.setPropertyName( inferredData.getPropertyName() ); + + collectionBinder.setBatchSize( property.getAnnotation( BatchSize.class ) ); + + collectionBinder.setJpaOrderBy( property.getAnnotation( jakarta.persistence.OrderBy.class ) ); + collectionBinder.setSqlOrderBy( getOverridableAnnotation(property, OrderBy.class, context) ); + + collectionBinder.setNaturalSort( property.getAnnotation( SortNatural.class ) ); + collectionBinder.setComparatorSort( property.getAnnotation( SortComparator.class ) ); + + collectionBinder.setCache( property.getAnnotation( Cache.class ) ); + collectionBinder.setPropertyHolder(propertyHolder); + Cascade hibernateCascade = property.getAnnotation( Cascade.class ); + NotFound notFound = property.getAnnotation( NotFound.class ); + collectionBinder.setIgnoreNotFound( notFound != null && notFound.action() == NotFoundAction.IGNORE ); + collectionBinder.setCollectionType( inferredData.getProperty().getElementClass() ); + collectionBinder.setAccessType( inferredData.getDefaultAccess() ); + + AnnotatedColumn[] elementColumns; + //do not use "element" if you are a JPA 2 @ElementCollection, only for legacy Hibernate mappings + PropertyData virtualProperty = property.isAnnotationPresent( ElementCollection.class ) + ? inferredData + : new WrappedInferredData( inferredData, "element" ); + Comment comment = property.getAnnotation(Comment.class); + if ( property.isAnnotationPresent( Column.class ) ) { + elementColumns = buildColumnFromAnnotation( + property.getAnnotation( Column.class ), + comment, + nullability, + propertyHolder, + virtualProperty, + entityBinder.getSecondaryTables(), + context + ); + } + else if ( property.isAnnotationPresent( Formula.class ) ) { + elementColumns = buildFormulaFromAnnotation( + getOverridableAnnotation( property, Formula.class, context ), + comment, + nullability, + propertyHolder, + virtualProperty, + entityBinder.getSecondaryTables(), + context + ); + } + else if ( property.isAnnotationPresent( Columns.class ) ) { + elementColumns = buildColumnsFromAnnotations( + property.getAnnotation( Columns.class ).columns(), + comment, + nullability, + propertyHolder, + virtualProperty, + entityBinder.getSecondaryTables(), + context + ); + } + else { + elementColumns = buildColumnFromNoAnnotation( + comment, + nullability, + propertyHolder, + virtualProperty, + entityBinder.getSecondaryTables(), + context + ); + } + + JoinColumn[] joinKeyColumns = mapKeyColumns( + propertyHolder, + inferredData, + entityBinder, + context, + property, + collectionBinder, + comment + ); + + AnnotatedJoinColumn[] mapJoinColumns = buildJoinColumnsWithDefaultColumnSuffix( + joinKeyColumns, + comment, + null, + entityBinder.getSecondaryTables(), + propertyHolder, + inferredData.getPropertyName(), + "_KEY", + context + ); + collectionBinder.setMapKeyManyToManyColumns( mapJoinColumns ); + + //potential element + collectionBinder.setEmbedded( property.isAnnotationPresent( Embedded.class ) ); + collectionBinder.setElementColumns( elementColumns ); + collectionBinder.setProperty(property); + + //TODO enhance exception with @ManyToAny and @CollectionOfElements + if ( oneToManyAnn != null && manyToManyAnn != null ) { + throw new AnnotationException( + "@OneToMany and @ManyToMany on the same property is not allowed: " + + propertyHolder.getEntityName() + "." + inferredData.getPropertyName() + ); + } + String mappedBy = null; + ReflectionManager reflectionManager = context.getBootstrapContext().getReflectionManager(); + if ( oneToManyAnn != null ) { + for ( AnnotatedJoinColumn column : joinColumns) { + if ( column.isSecondary() ) { + throw new NotYetImplementedException( "Collections having FK in secondary table" ); + } + } + collectionBinder.setFkJoinColumns(joinColumns); + mappedBy = oneToManyAnn.mappedBy(); + //noinspection unchecked + collectionBinder.setTargetEntity( reflectionManager.toXClass( oneToManyAnn.targetEntity() ) ); + collectionBinder.setCascadeStrategy( + getCascadeStrategy( oneToManyAnn.cascade(), hibernateCascade, oneToManyAnn.orphanRemoval(), false ) + ); + collectionBinder.setOneToMany( true ); + } + else if ( elementCollectionAnn != null ) { + for ( AnnotatedJoinColumn column : joinColumns) { + if ( column.isSecondary() ) { + throw new NotYetImplementedException( "Collections having FK in secondary table" ); + } + } + collectionBinder.setFkJoinColumns(joinColumns); + mappedBy = ""; + final Class targetElement = elementCollectionAnn.targetClass(); + collectionBinder.setTargetEntity( reflectionManager.toXClass( targetElement ) ); + //collectionBinder.setCascadeStrategy( getCascadeStrategy( embeddedCollectionAnn.cascade(), hibernateCascade ) ); + collectionBinder.setOneToMany( true ); + } + else if ( manyToManyAnn != null ) { + mappedBy = manyToManyAnn.mappedBy(); + //noinspection unchecked + collectionBinder.setTargetEntity( reflectionManager.toXClass( manyToManyAnn.targetEntity() ) ); + collectionBinder.setCascadeStrategy( + getCascadeStrategy( manyToManyAnn.cascade(), hibernateCascade, false, false ) + ); + collectionBinder.setOneToMany( false ); + } + else if ( property.isAnnotationPresent( ManyToAny.class ) ) { + mappedBy = ""; + collectionBinder.setTargetEntity( reflectionManager.toXClass( void.class ) ); + collectionBinder.setCascadeStrategy( + getCascadeStrategy( null, hibernateCascade, false, false ) + ); + collectionBinder.setOneToMany( false ); + } + collectionBinder.setMappedBy( mappedBy ); + + bindJoinedTableAssociation( + property, + context, + entityBinder, + collectionBinder, + propertyHolder, + inferredData, + mappedBy + ); + + OnDelete onDeleteAnn = property.getAnnotation( OnDelete.class ); + boolean onDeleteCascade = onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(); + collectionBinder.setCascadeDeleteEnabled( onDeleteCascade ); + if (isIdentifierMapper) { + collectionBinder.setInsertable( false ); + collectionBinder.setUpdatable( false ); + } + if ( property.isAnnotationPresent( CollectionId.class ) ) { //do not compute the generators unless necessary + HashMap localGenerators = new HashMap<>(classGenerators); + localGenerators.putAll( buildGenerators(property, context) ); + collectionBinder.setLocalGenerators( localGenerators ); + + } + collectionBinder.setInheritanceStatePerClass(inheritanceStatePerClass); + collectionBinder.setDeclaringClass( inferredData.getDeclaringClass() ); + collectionBinder.bind(); + } + + private static boolean hasMapKeyAnnotation(XProperty property) { + return property.isAnnotationPresent(MapKeyJavaType.class) + || property.isAnnotationPresent(MapKeyJdbcType.class) + || property.isAnnotationPresent(MapKeyJdbcTypeCode.class) + || property.isAnnotationPresent(MapKeyMutability.class) + || property.isAnnotationPresent(MapKey.class) + || property.isAnnotationPresent(MapKeyCustomType.class); + } + + private static void bindAny( + PropertyHolder propertyHolder, + Nullability nullability, + PropertyData inferredData, + EntityBinder entityBinder, + boolean isIdentifierMapper, + MetadataBuildingContext context, + XProperty property, + AnnotatedJoinColumn[] joinColumns, + boolean forcePersist) { + + //check validity + if ( property.isAnnotationPresent( Columns.class ) ) { + throw new AnnotationException( + String.format( + Locale.ROOT, + "@Columns not allowed on a @Any property [%s]; @Column or @Formula is used to map the discriminator" + + "and only one is allowed", + BinderHelper.getPath(propertyHolder, inferredData) + ) + ); + } + + Cascade hibernateCascade = property.getAnnotation( Cascade.class ); + 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() ); + } + } + bindAny( + getCascadeStrategy( null, hibernateCascade, false, forcePersist), + //@Any has not cascade attribute + joinColumns, + onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(), + nullability, + propertyHolder, + inferredData, + entityBinder, + isIdentifierMapper, + context + ); + } + + 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( + "@Column(s) not allowed on a @OneToOne property: " + + BinderHelper.getPath(propertyHolder, inferredData) + ); + } + + //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 ); + boolean ignoreNotFound = notFound != null && notFound.action() == NotFoundAction.IGNORE; + // 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 ) && !ignoreNotFound; + matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), ignoreNotFound, 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() ); + } + } + bindOneToOne( + getCascadeStrategy( ann.cascade(), hibernateCascade, ann.orphanRemoval(), forcePersist), + joinColumns, + !mandatory, + getFetchMode( ann.fetch() ), + ignoreNotFound, + 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( + "@Column(s) not allowed on a @ManyToOne property: " + + BinderHelper.getPath(propertyHolder, inferredData) + ); + } + + Cascade hibernateCascade = property.getAnnotation( Cascade.class ); + NotFound notFound = property.getAnnotation( NotFound.class ); + boolean ignoreNotFound = notFound != null && notFound.action() == NotFoundAction.IGNORE; + matchIgnoreNotFoundWithFetchType( propertyHolder.getEntityName(), property.getName(), ignoreNotFound, 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 ) && !ignoreNotFound; + bindManyToOne( + getCascadeStrategy( ann.cascade(), hibernateCascade, false, forcePersist), + joinColumns, + !mandatory, + ignoreNotFound, + onDeleteAnn != null && OnDeleteAction.CASCADE == onDeleteAnn.action(), + ToOneBinder.getTargetEntity(inferredData, context), + propertyHolder, + inferredData, + false, + isIdentifierMapper, + inSecondPass, + propertyBinder, + context + ); + } + private static JoinColumn[] mapKeyColumns( PropertyHolder propertyHolder, PropertyData inferredData, @@ -2575,17 +2705,13 @@ public final class AnnotationBinder { XProperty property, CollectionBinder collectionBinder, Comment comment) { - Column[] keyColumns; - if ( property.isAnnotationPresent( MapKeyColumn.class ) ) { - keyColumns = new Column[] { new MapKeyColumnDelegator( property.getAnnotation( MapKeyColumn.class ) ) }; - } - else { - keyColumns = null; - } - AnnotatedColumn[] mapColumns = buildColumnFromAnnotation( + Column[] keyColumns = property.isAnnotationPresent(MapKeyColumn.class) + ? new Column[]{ new MapKeyColumnDelegator( property.getAnnotation(MapKeyColumn.class) ) } + : null; + + AnnotatedColumn[] mapColumns = buildColumnsFromAnnotations( keyColumns, - null, comment, Nullability.FORCED_NOT_NULL, propertyHolder, @@ -2727,7 +2853,7 @@ public final class AnnotationBinder { PropertyHolder propertyHolder, PropertyData inferredData, SimpleValue idValue, - HashMap classGenerators, + Map classGenerators, boolean isIdentifierMapper, MetadataBuildingContext buildingContext) { if ( isIdentifierMapper ) { @@ -2739,7 +2865,8 @@ public final class AnnotationBinder { XClass entityXClass = inferredData.getClassOrElement(); XProperty idXProperty = inferredData.getProperty(); - final Annotation generatorAnnotation = HCANNHelper.findContainingAnnotation( idXProperty, IdGeneratorType.class, buildingContext ); + final Annotation generatorAnnotation = + HCANNHelper.findContainingAnnotation( idXProperty, IdGeneratorType.class, buildingContext ); if ( generatorAnnotation != null ) { idValue.setCustomIdGeneratorCreator( new CustomIdGeneratorCreator( generatorAnnotation, idXProperty ) ); } @@ -2777,7 +2904,7 @@ public final class AnnotationBinder { //clone classGenerator and override with local values HashMap localGenerators = new HashMap<>( classGenerators ); localGenerators.putAll( buildGenerators( idXProperty, buildingContext ) ); - BinderHelper.makeIdGenerator( + makeIdGenerator( idValue, idXProperty, generatorType, @@ -2797,27 +2924,28 @@ public final class AnnotationBinder { GeneratedValue generatedValueAnn, final MetadataBuildingContext buildingContext, final XClass javaTypeXClass) { - return buildingContext.getBuildingOptions().getIdGenerationTypeInterpreter().determineGeneratorName( - generatedValueAnn.strategy(), - new IdGeneratorStrategyInterpreter.GeneratorNameDeterminationContext() { - Class javaType = null; - @Override - public Class getIdType() { - if ( javaType == null ) { - javaType = buildingContext - .getBootstrapContext() - .getReflectionManager() - .toClass( javaTypeXClass ); - } - return javaType; - } + return buildingContext.getBuildingOptions().getIdGenerationTypeInterpreter() + .determineGeneratorName( + generatedValueAnn.strategy(), + new IdGeneratorStrategyInterpreter.GeneratorNameDeterminationContext() { + Class javaType = null; + @Override + public Class getIdType() { + if ( javaType == null ) { + javaType = buildingContext + .getBootstrapContext() + .getReflectionManager() + .toClass( javaTypeXClass ); + } + return javaType; + } - @Override - public String getGeneratedValueGeneratorName() { - return generatedValueAnn.generator(); - } - } - ); + @Override + public String getGeneratedValueGeneratorName() { + return generatedValueAnn.generator(); + } + } + ); } //TODO move that to collection binder? @@ -2889,7 +3017,7 @@ public final class AnnotationBinder { annJoins = null; annInverseJoins = null; } - AnnotatedJoinColumn[] joinColumns = AnnotatedJoinColumn.buildJoinTableJoinColumns( + AnnotatedJoinColumn[] joinColumns = buildJoinTableJoinColumns( annJoins, entityBinder.getSecondaryTables(), propertyHolder, @@ -2897,7 +3025,7 @@ public final class AnnotationBinder { mappedBy, buildingContext ); - AnnotatedJoinColumn[] inverseJoinColumns = AnnotatedJoinColumn.buildJoinTableJoinColumns( + AnnotatedJoinColumn[] inverseJoinColumns = buildJoinTableJoinColumns( annInverseJoins, entityBinder.getSecondaryTables(), propertyHolder, @@ -2926,7 +3054,14 @@ public final class AnnotationBinder { AnnotatedJoinColumn[] columns) { Component comp; if ( referencedEntityName != null ) { - comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, customInstantiatorImpl, buildingContext ); + comp = createComponent( + propertyHolder, + inferredData, + isComponentEmbedded, + isIdentifierMapper, + customInstantiatorImpl, + buildingContext + ); SecondPass sp = new CopyIdentifierComponentSecondPass( comp, referencedEntityName, @@ -2937,9 +3072,17 @@ public final class AnnotationBinder { } else { comp = fillComponent( - propertyHolder, inferredData, propertyAccessor, !isId, entityBinder, - isComponentEmbedded, isIdentifierMapper, - false, customInstantiatorImpl, buildingContext, inheritanceStatePerClass + propertyHolder, + inferredData, + propertyAccessor, + !isId, + entityBinder, + isComponentEmbedded, + isIdentifierMapper, + false, + customInstantiatorImpl, + buildingContext, + inheritanceStatePerClass ); } if ( isId ) { @@ -3021,7 +3164,14 @@ public final class AnnotationBinder { * Because it's a value type, there is no bidirectional association, hence second pass * ordering does not matter */ - Component comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, customInstantiatorImpl, buildingContext ); + Component comp = createComponent( + propertyHolder, + inferredData, + isComponentEmbedded, + isIdentifierMapper, + customInstantiatorImpl, + buildingContext + ); String subpath = BinderHelper.getPath( propertyHolder, inferredData ); LOG.tracev( "Binding component with path: {0}", subpath ); @@ -3051,8 +3201,11 @@ public final class AnnotationBinder { // iterate from base returned class up hierarchy to handle cases where the @Id attributes // might be spread across the subclasses and super classes. while ( !Object.class.getName().equals( baseReturnedClassOrElement.getName() ) ) { - PropertyContainer propContainer = new PropertyContainer( baseReturnedClassOrElement, xClassProcessed, propertyAccessor ); - addElementsOfClass( baseClassElements, propContainer, buildingContext ); + addElementsOfClass( + baseClassElements, + new PropertyContainer( baseReturnedClassOrElement, xClassProcessed, propertyAccessor ), + buildingContext + ); for ( PropertyData element : baseClassElements ) { orderedBaseClassElements.put( element.getPropertyName(), element ); } @@ -3077,7 +3230,8 @@ public final class AnnotationBinder { if ( !hasAnnotationsOnIdClass( xClassProcessed ) ) { for ( int i = 0; i < classElements.size(); i++ ) { final PropertyData idClassPropertyData = classElements.get( i ); - final PropertyData entityPropertyData = orderedBaseClassElements.get( idClassPropertyData.getPropertyName() ); + final PropertyData entityPropertyData = + orderedBaseClassElements.get( idClassPropertyData.getPropertyName() ); if ( propertyHolder.isInIdClass() ) { if ( entityPropertyData == null ) { throw new AnnotationException( @@ -3086,9 +3240,7 @@ public final class AnnotationBinder { + idClassPropertyData.getPropertyName() ); } - final boolean hasXToOneAnnotation = entityPropertyData.getProperty() - .isAnnotationPresent( ManyToOne.class ) - || entityPropertyData.getProperty().isAnnotationPresent( OneToOne.class ); + final boolean hasXToOneAnnotation = hasToOneAnnotation( entityPropertyData.getProperty() ); final boolean isOfDifferentType = !entityPropertyData.getClassOrElement() .equals( idClassPropertyData.getClassOrElement() ); if ( !hasXToOneAnnotation || !isOfDifferentType ) { @@ -3122,15 +3274,13 @@ public final class AnnotationBinder { ); XProperty property = propertyAnnotatedElement.getProperty(); - if ( property.isAnnotationPresent( GeneratedValue.class ) && - property.isAnnotationPresent( Id.class ) ) { + if ( property.isAnnotationPresent( GeneratedValue.class ) + && property.isAnnotationPresent( Id.class ) ) { GeneratedValue generatedValue = property.getAnnotation( GeneratedValue.class ); String generatorType = generatedValue != null ? generatorType( generatedValue, buildingContext, property.getType() ) - : "assigned"; - String generator = generatedValue != null ? - generatedValue.generator() : - BinderHelper.ANNOTATION_STRING_DEFAULT; + : DEFAULT_ID_GEN_STRATEGY; + String generator = generatedValue != null ? generatedValue.generator() : ""; if ( isGlobalGeneratorNameGlobal( buildingContext ) ) { buildGenerators( property, buildingContext ); @@ -3147,8 +3297,9 @@ public final class AnnotationBinder { bindEmbeddableInstantiatorRegistrations( property, buildingContext ); } else { - Map localGenerators = new HashMap<>( buildGenerators( property, buildingContext ) ); - BinderHelper.makeIdGenerator( + Map localGenerators = + new HashMap<>( buildGenerators( property, buildingContext ) ); + makeIdGenerator( (SimpleValue) comp.getProperty( property.getName() ).getValue(), property, generatorType, @@ -3162,6 +3313,11 @@ public final class AnnotationBinder { return comp; } + private static boolean hasToOneAnnotation(XAnnotatedElement property) { + return property.isAnnotationPresent(ManyToOne.class) + || property.isAnnotationPresent(OneToOne.class); + } + public static Component createComponent( PropertyHolder propertyHolder, PropertyData inferredData, @@ -3239,7 +3395,7 @@ public final class AnnotationBinder { buildingContext.getMetadataCollector().addSecondPass( secondPass ); } else { - BinderHelper.makeIdGenerator( + makeIdGenerator( id, inferredData.getProperty(), DEFAULT_ID_GEN_STRATEGY, @@ -3706,22 +3862,25 @@ public final class AnnotationBinder { value.disableForeignKey(); } else if ( fkOverride != null ) { - value.setForeignKeyName( StringHelper.nullIfEmpty( fkOverride.name() ) ); - value.setForeignKeyDefinition( StringHelper.nullIfEmpty( fkOverride.foreignKeyDefinition() ) ); + value.setForeignKeyName( nullIfEmpty( fkOverride.name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( fkOverride.foreignKeyDefinition() ) ); } else if ( joinColumns != null ) { - value.setForeignKeyName( StringHelper.nullIfEmpty( joinColumns.foreignKey().name() ) ); - value.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumns.foreignKey().foreignKeyDefinition() ) ); + value.setForeignKeyName( nullIfEmpty( joinColumns.foreignKey().name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( joinColumns.foreignKey().foreignKeyDefinition() ) ); } else if ( joinColumn != null ) { - value.setForeignKeyName( StringHelper.nullIfEmpty( joinColumn.foreignKey().name() ) ); - value.setForeignKeyDefinition( StringHelper.nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); + value.setForeignKeyName( nullIfEmpty( joinColumn.foreignKey().name() ) ); + value.setForeignKeyDefinition( nullIfEmpty( joinColumn.foreignKey().foreignKeyDefinition() ) ); } } } } - private static HashMap buildGenerators(XAnnotatedElement annElt, MetadataBuildingContext context) { + private static HashMap buildGenerators( + XAnnotatedElement annElt, + MetadataBuildingContext context) { + InFlightMetadataCollector metadataCollector = context.getMetadataCollector(); HashMap generators = new HashMap<>(); @@ -3792,30 +3951,18 @@ public final class AnnotationBinder { public static Map buildInheritanceStates( List orderedClasses, MetadataBuildingContext buildingContext) { - Map inheritanceStatePerClass = new HashMap<>( - orderedClasses.size() - ); + Map inheritanceStatePerClass = new HashMap<>( orderedClasses.size() ); for ( XClass clazz : orderedClasses ) { - InheritanceState superclassState = InheritanceState.getSuperclassInheritanceState( - clazz, inheritanceStatePerClass - ); + InheritanceState superclassState = getSuperclassInheritanceState( clazz, inheritanceStatePerClass ); InheritanceState state = new InheritanceState( clazz, inheritanceStatePerClass, buildingContext ); if ( superclassState != null ) { //the classes are ordered thus preventing an NPE //FIXME if an entity has subclasses annotated @MappedSuperclass wo sub @Entity this is wrong superclassState.setHasSiblings( true ); - InheritanceState superEntityState = InheritanceState.getInheritanceStateOfSuperEntity( - clazz, inheritanceStatePerClass - ); + InheritanceState superEntityState = getInheritanceStateOfSuperEntity( clazz, inheritanceStatePerClass ); state.setHasParents( superEntityState != null ); - final boolean nonDefault = state.getType() != null && !InheritanceType.SINGLE_TABLE - .equals( state.getType() ); + logMixedInheritance( clazz, superclassState, state ); if ( superclassState.getType() != null ) { - final boolean mixingStrategy = state.getType() != null && !state.getType() - .equals( superclassState.getType() ); - if ( nonDefault && mixingStrategy ) { - LOG.invalidSubStrategy( clazz.getName() ); - } state.setType( superclassState.getType() ); } } @@ -3824,33 +3971,43 @@ public final class AnnotationBinder { return inheritanceStatePerClass; } + private static void logMixedInheritance(XClass clazz, InheritanceState superclassState, InheritanceState state) { + if ( state.getType() != null && superclassState.getType() != null ) { + final boolean nonDefault = InheritanceType.SINGLE_TABLE != state.getType(); + final boolean mixingStrategy = state.getType() != superclassState.getType(); + if ( nonDefault && mixingStrategy ) { + LOG.invalidSubStrategy( clazz.getName() ); + } + } + } + private static boolean hasAnnotationsOnIdClass(XClass idClass) { // if(idClass.getAnnotation(Embeddable.class) != null) // return true; - List properties = idClass.getDeclaredProperties( XClass.ACCESS_FIELD ); - for ( XProperty property : properties ) { - if ( property.isAnnotationPresent( Column.class ) || property.isAnnotationPresent( OneToMany.class ) || - property.isAnnotationPresent( ManyToOne.class ) || property.isAnnotationPresent( Id.class ) || - property.isAnnotationPresent( GeneratedValue.class ) || property.isAnnotationPresent( OneToOne.class ) || - property.isAnnotationPresent( ManyToMany.class ) - ) { + for ( XProperty property : idClass.getDeclaredProperties( XClass.ACCESS_FIELD ) ) { + if ( hasTriggeringAnnotation(property) ) { return true; } } - List methods = idClass.getDeclaredMethods(); - for ( XMethod method : methods ) { - if ( method.isAnnotationPresent( Column.class ) || method.isAnnotationPresent( OneToMany.class ) || - method.isAnnotationPresent( ManyToOne.class ) || method.isAnnotationPresent( Id.class ) || - method.isAnnotationPresent( GeneratedValue.class ) || method.isAnnotationPresent( OneToOne.class ) || - method.isAnnotationPresent( ManyToMany.class ) - ) { + for ( XMethod method : idClass.getDeclaredMethods() ) { + if ( hasTriggeringAnnotation(method) ) { return true; } } return false; } + private static boolean hasTriggeringAnnotation(XAnnotatedElement property) { + return property.isAnnotationPresent(Column.class) + || property.isAnnotationPresent(OneToMany.class) + || property.isAnnotationPresent(ManyToOne.class) + || property.isAnnotationPresent(Id.class) + || property.isAnnotationPresent(GeneratedValue.class) + || property.isAnnotationPresent(OneToOne.class) + || property.isAnnotationPresent(ManyToMany.class); + } + private static void matchIgnoreNotFoundWithFetchType( String entity, String association, @@ -3872,7 +4029,8 @@ public final class AnnotationBinder { @Override public IdentifierGenerator createGenerator(CustomIdGeneratorCreationContext context) { - final IdGeneratorType idGeneratorType = generatorAnnotation.annotationType().getAnnotation( IdGeneratorType.class ); + final IdGeneratorType idGeneratorType = + generatorAnnotation.annotationType().getAnnotation( IdGeneratorType.class ); assert idGeneratorType != null; final Class generatorClass = idGeneratorType.value(); 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 b94cc71dac..74b4acd358 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/BinderHelper.java @@ -65,6 +65,8 @@ import jakarta.persistence.SequenceGenerator; import jakarta.persistence.TableGenerator; import jakarta.persistence.UniqueConstraint; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnOrFormulaFromAnnotation; + /** * @author Emmanuel Bernard */ @@ -817,8 +819,8 @@ public class BinderHelper { final BasicValueBinder discriminatorValueBinder = new BasicValueBinder<>( BasicValueBinder.Kind.ANY_DISCRIMINATOR, context ); - final AnnotatedColumn[] discriminatorColumns = AnnotatedColumn.buildColumnFromAnnotation( - new jakarta.persistence.Column[] { discriminatorColumn }, + final AnnotatedColumn[] discriminatorColumns = buildColumnOrFormulaFromAnnotation( + discriminatorColumn, discriminatorFormula, null, nullability, @@ -947,8 +949,8 @@ public class BinderHelper { } return pd; } - String propertyPath = isId ? "" : propertyName; - return buildingContext.getMetadataCollector().getPropertyAnnotatedWithMapsId( persistentXClass, propertyPath ); + return buildingContext.getMetadataCollector() + .getPropertyAnnotatedWithMapsId( persistentXClass, isId ? "" : propertyName); } public static Map toAliasTableMap(SqlFragmentAlias[] aliases){ diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/ColumnsBuilder.java b/hibernate-core/src/main/java/org/hibernate/cfg/ColumnsBuilder.java index 4076488e6e..a800a826f6 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/ColumnsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/ColumnsBuilder.java @@ -28,6 +28,10 @@ import org.hibernate.cfg.annotations.EntityBinder; import org.hibernate.cfg.annotations.Nullability; import org.hibernate.internal.util.StringHelper; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromAnnotation; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromNoAnnotation; +import static org.hibernate.cfg.AnnotatedColumn.buildColumnsFromAnnotations; +import static org.hibernate.cfg.AnnotatedColumn.buildFormulaFromAnnotation; import static org.hibernate.cfg.AnnotationBinder.getOverridableAnnotation; /** @@ -77,12 +81,20 @@ class ColumnsBuilder { Comment comment = property.getAnnotation(Comment.class); - if ( property.isAnnotationPresent( Column.class ) || property.isAnnotationPresent( Formula.class ) ) { - Column ann = property.getAnnotation( Column.class ); - Formula formulaAnn = getOverridableAnnotation( property, Formula.class, buildingContext ); - columns = AnnotatedColumn.buildColumnFromAnnotation( - new Column[] { ann }, - formulaAnn, + if ( property.isAnnotationPresent( Column.class ) ) { + columns = buildColumnFromAnnotation( + property.getAnnotation( Column.class ), + comment, + nullability, + propertyHolder, + inferredData, + entityBinder.getSecondaryTables(), + buildingContext + ); + } + else if ( property.isAnnotationPresent( Formula.class ) ) { + columns = buildFormulaFromAnnotation( + getOverridableAnnotation( property, Formula.class, buildingContext ), comment, nullability, propertyHolder, @@ -92,10 +104,8 @@ class ColumnsBuilder { ); } else if ( property.isAnnotationPresent( Columns.class ) ) { - Columns anns = property.getAnnotation( Columns.class ); - columns = AnnotatedColumn.buildColumnFromAnnotation( - anns.columns(), - null, + columns = buildColumnsFromAnnotations( + property.getAnnotation( Columns.class ).columns(), comment, nullability, propertyHolder, @@ -110,20 +120,17 @@ class ColumnsBuilder { ( property.isAnnotationPresent( ManyToOne.class ) || property.isAnnotationPresent( OneToOne.class ) ) ) { - joinColumns = buildDefaultJoinColumnsForXToOne(property, inferredData); + joinColumns = buildDefaultJoinColumnsForXToOne( property, inferredData ); } else if ( joinColumns == null && ( property.isAnnotationPresent( OneToMany.class ) || property.isAnnotationPresent( ElementCollection.class ) ) ) { OneToMany oneToMany = property.getAnnotation( OneToMany.class ); - String mappedBy = oneToMany != null ? - oneToMany.mappedBy() : - ""; joinColumns = AnnotatedJoinColumn.buildJoinColumns( null, comment, - mappedBy, + oneToMany != null ? oneToMany.mappedBy() : "", entityBinder.getSecondaryTables(), propertyHolder, inferredData.getPropertyName(), @@ -136,9 +143,7 @@ class ColumnsBuilder { } if ( columns == null && !property.isAnnotationPresent( ManyToMany.class ) ) { //useful for collection of embedded elements - columns = AnnotatedColumn.buildColumnFromAnnotation( - null, - null, + columns = buildColumnFromNoAnnotation( comment, nullability, propertyHolder, 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 8249645661..b594b637dc 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 @@ -84,7 +84,6 @@ import org.hibernate.cfg.PropertyInferredData; import org.hibernate.cfg.PropertyPreloadedData; import org.hibernate.cfg.SecondPass; import org.hibernate.engine.config.spi.ConfigurationService; -import org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; @@ -130,9 +129,13 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.OrderColumn; import static jakarta.persistence.AccessType.PROPERTY; +import static org.hibernate.cfg.AnnotatedColumn.checkPropertyConsistency; +import static org.hibernate.cfg.AnnotationBinder.fillComponent; import static org.hibernate.cfg.AnnotationBinder.getOverridableAnnotation; +import static org.hibernate.cfg.BinderHelper.isEmptyAnnotationValue; import static org.hibernate.cfg.BinderHelper.toAliasEntityMap; import static org.hibernate.cfg.BinderHelper.toAliasTableMap; +import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.fromExternalName; /** * Base class for binding different types of collections to Hibernate configuration objects. @@ -656,19 +659,7 @@ public abstract class CollectionBinder { ); } - // set explicit type information - final InFlightMetadataCollector metadataCollector = buildingContext.getMetadataCollector(); - if ( explicitType != null ) { - final TypeDefinition typeDef = metadataCollector.getTypeDefinition( explicitType ); - if ( typeDef == null ) { - collection.setTypeName( explicitType ); - collection.setTypeParameters( explicitTypeParameters ); - } - else { - collection.setTypeName( typeDef.getTypeImplementorClass().getName() ); - collection.setTypeParameters( typeDef.getParameters() ); - } - } + bindExplicitTypes(); //set laziness defineFetchingStrategy(); @@ -677,79 +668,24 @@ public abstract class CollectionBinder { collection.setMutable( !property.isAnnotationPresent( Immutable.class ) ); //work on association - boolean isMappedBy = !BinderHelper.isEmptyAnnotationValue( mappedBy ); + boolean isMappedBy = !isEmptyAnnotationValue( mappedBy ); - final OptimisticLock lockAnn = property.getAnnotation( OptimisticLock.class ); - final boolean includeInOptimisticLockChecks = ( lockAnn != null ) - ? ! lockAnn.excluded() - : ! isMappedBy; - collection.setOptimisticLocked( includeInOptimisticLockChecks ); + bindOptimisticLock( isMappedBy ); - Persister persisterAnn = property.getAnnotation( Persister.class ); - if ( persisterAnn != null ) { - //noinspection rawtypes - collection.setCollectionPersisterClass( (Class) persisterAnn.impl() ); - } + bindCustomPersister(); applySortingAndOrdering( collection ); - //set cache - if ( StringHelper.isNotEmpty( cacheConcurrencyStrategy ) ) { - collection.setCacheConcurrencyStrategy( cacheConcurrencyStrategy ); - collection.setCacheRegionName( cacheRegionName ); - } + bindCache(); - //SQL overriding - SQLInsert sqlInsert = property.getAnnotation( SQLInsert.class ); - SQLUpdate sqlUpdate = property.getAnnotation( SQLUpdate.class ); - SQLDelete sqlDelete = property.getAnnotation( SQLDelete.class ); - SQLDeleteAll sqlDeleteAll = property.getAnnotation( SQLDeleteAll.class ); - Loader loader = property.getAnnotation( Loader.class ); - if ( sqlInsert != null ) { - collection.setCustomSQLInsert( sqlInsert.sql().trim(), sqlInsert.callable(), - ExecuteUpdateResultCheckStyle.fromExternalName( sqlInsert.check().toString().toLowerCase(Locale.ROOT) ) - ); + bindLoader(); - } - if ( sqlUpdate != null ) { - collection.setCustomSQLUpdate( sqlUpdate.sql(), sqlUpdate.callable(), - ExecuteUpdateResultCheckStyle.fromExternalName( sqlUpdate.check().toString().toLowerCase(Locale.ROOT) ) - ); - } - if ( sqlDelete != null ) { - collection.setCustomSQLDelete( sqlDelete.sql(), sqlDelete.callable(), - ExecuteUpdateResultCheckStyle.fromExternalName( sqlDelete.check().toString().toLowerCase(Locale.ROOT) ) - ); - } - if ( sqlDeleteAll != null ) { - collection.setCustomSQLDeleteAll( sqlDeleteAll.sql(), sqlDeleteAll.callable(), - ExecuteUpdateResultCheckStyle.fromExternalName( sqlDeleteAll.check().toString().toLowerCase(Locale.ROOT) ) - ); - } - if ( loader != null ) { - collection.setLoaderName( loader.namedQuery() ); - } - - if (isMappedBy - && (property.isAnnotationPresent( JoinColumn.class ) - || property.isAnnotationPresent( JoinColumns.class ) - || propertyHolder.getJoinTable( property ) != null ) ) { - String message = "Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: "; - message += StringHelper.qualify( propertyHolder.getPath(), propertyName ); - throw new AnnotationException( message ); - } - - if (!isMappedBy - && oneToMany - && property.isAnnotationPresent( OnDelete.class ) - && !property.isAnnotationPresent( JoinColumn.class )) { - String message = "Unidirectional one-to-many associations annotated with @OnDelete must define @JoinColumn: "; - message += StringHelper.qualify( propertyHolder.getPath(), propertyName ); - throw new AnnotationException( message ); - } + detectMappedByProblem( isMappedBy ); collection.setInverse( isMappedBy ); + final InFlightMetadataCollector metadataCollector = buildingContext.getMetadataCollector(); + //many to many may need some second pass information if ( !oneToMany && isMappedBy ) { metadataCollector.addMappedBy( getCollectionType().getName(), mappedBy, propertyName ); @@ -788,6 +724,68 @@ public abstract class CollectionBinder { metadataCollector.addCollectionBinding( collection ); + bindProperty(); + } + + private void bindCustomPersister() { + Persister persisterAnn = property.getAnnotation( Persister.class ); + if ( persisterAnn != null ) { + //noinspection rawtypes + collection.setCollectionPersisterClass( (Class) persisterAnn.impl() ); + } + } + + private void bindOptimisticLock(boolean isMappedBy) { + final OptimisticLock lockAnn = property.getAnnotation( OptimisticLock.class ); + final boolean includeInOptimisticLockChecks = lockAnn != null ? !lockAnn.excluded() : !isMappedBy; + collection.setOptimisticLocked( includeInOptimisticLockChecks ); + } + + private void bindCache() { + //set cache + if ( StringHelper.isNotEmpty( cacheConcurrencyStrategy ) ) { + collection.setCacheConcurrencyStrategy( cacheConcurrencyStrategy ); + collection.setCacheRegionName( cacheRegionName ); + } + } + + private void bindExplicitTypes() { + // set explicit type information + final InFlightMetadataCollector metadataCollector = buildingContext.getMetadataCollector(); + if ( explicitType != null ) { + final TypeDefinition typeDef = metadataCollector.getTypeDefinition( explicitType ); + if ( typeDef == null ) { + collection.setTypeName( explicitType ); + collection.setTypeParameters( explicitTypeParameters ); + } + else { + collection.setTypeName( typeDef.getTypeImplementorClass().getName() ); + collection.setTypeParameters( typeDef.getParameters() ); + } + } + } + + private void detectMappedByProblem(boolean isMappedBy) { + if (isMappedBy + && (property.isAnnotationPresent( JoinColumn.class ) + || property.isAnnotationPresent( JoinColumns.class ) + || propertyHolder.getJoinTable( property ) != null ) ) { + String message = "Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: "; + message += StringHelper.qualify( propertyHolder.getPath(), propertyName ); + throw new AnnotationException( message ); + } + + if (!isMappedBy + && oneToMany + && property.isAnnotationPresent( OnDelete.class ) + && !property.isAnnotationPresent( JoinColumn.class )) { + String message = "Unidirectional one-to-many associations annotated with @OnDelete must define @JoinColumn: "; + message += StringHelper.qualify( propertyHolder.getPath(), propertyName ); + throw new AnnotationException( message ); + } + } + + private void bindProperty() { //property building PropertyBinder binder = new PropertyBinder(); binder.setName( propertyName ); @@ -813,6 +811,47 @@ public abstract class CollectionBinder { propertyHolder.addProperty( prop, declaringClass ); } + private void bindLoader() { + //SQL overriding + SQLInsert sqlInsert = property.getAnnotation( SQLInsert.class ); + SQLUpdate sqlUpdate = property.getAnnotation( SQLUpdate.class ); + SQLDelete sqlDelete = property.getAnnotation( SQLDelete.class ); + SQLDeleteAll sqlDeleteAll = property.getAnnotation( SQLDeleteAll.class ); + Loader loader = property.getAnnotation( Loader.class ); + if ( sqlInsert != null ) { + collection.setCustomSQLInsert( + sqlInsert.sql().trim(), + sqlInsert.callable(), + fromExternalName( sqlInsert.check().toString().toLowerCase(Locale.ROOT) ) + ); + + } + if ( sqlUpdate != null ) { + collection.setCustomSQLUpdate( + sqlUpdate.sql(), + sqlUpdate.callable(), + fromExternalName( sqlUpdate.check().toString().toLowerCase(Locale.ROOT) ) + ); + } + if ( sqlDelete != null ) { + collection.setCustomSQLDelete( + sqlDelete.sql(), + sqlDelete.callable(), + fromExternalName( sqlDelete.check().toString().toLowerCase(Locale.ROOT) ) + ); + } + if ( sqlDeleteAll != null ) { + collection.setCustomSQLDeleteAll( + sqlDeleteAll.sql(), + sqlDeleteAll.callable(), + fromExternalName( sqlDeleteAll.check().toString().toLowerCase(Locale.ROOT) ) + ); + } + if ( loader != null ) { + collection.setLoaderName( loader.namedQuery() ); + } + } + private void applySortingAndOrdering(Collection collection) { final boolean hadExplicitSort; final Class> comparatorClass; @@ -1033,10 +1072,10 @@ public abstract class CollectionBinder { MetadataBuildingContext buildingContext) { PersistentClass persistentClass = persistentClasses.get( collType.getName() ); boolean reversePropertyInJoin = false; - if ( persistentClass != null && StringHelper.isNotEmpty( this.mappedBy ) ) { + if ( persistentClass != null && StringHelper.isNotEmpty( mappedBy ) ) { try { reversePropertyInJoin = 0 != persistentClass.getJoinNumber( - persistentClass.getRecursiveProperty( this.mappedBy ) + persistentClass.getRecursiveProperty( mappedBy ) ); } catch (MappingException e) { @@ -1054,7 +1093,7 @@ public abstract class CollectionBinder { && !reversePropertyInJoin && oneToMany && !this.isExplicitAssociationTable - && ( joinColumns[0].isImplicit() && !BinderHelper.isEmptyAnnotationValue( this.mappedBy ) //implicit @JoinColumn + && ( joinColumns[0].isImplicit() && !isEmptyAnnotationValue( this.mappedBy ) //implicit @JoinColumn || !fkJoinColumns[0].isImplicit() ) //this is an explicit @JoinColumn ) { //this is a Foreign key @@ -1108,22 +1147,15 @@ public abstract class CollectionBinder { "CollectionSecondPass for oneToMany should not be called with null mappings" ); } - org.hibernate.mapping.OneToMany oneToMany = new org.hibernate.mapping.OneToMany( buildingContext, collection.getOwner() ); + org.hibernate.mapping.OneToMany oneToMany = + new org.hibernate.mapping.OneToMany( buildingContext, collection.getOwner() ); collection.setElement( oneToMany ); oneToMany.setReferencedEntityName( collectionType.getName() ); oneToMany.setIgnoreNotFound( ignoreNotFound ); String assocClass = oneToMany.getReferencedEntityName(); PersistentClass associatedClass = persistentClasses.get( assocClass ); - if ( jpaOrderBy != null ) { - final String orderByFragment = buildOrderByClauseFromHql( - jpaOrderBy.value(), - associatedClass - ); - if ( StringHelper.isNotEmpty( orderByFragment ) ) { - collection.setOrderBy( orderByFragment ); - } - } + handleJpaOrderBy( collection, associatedClass ); Map joins = buildingContext.getMetadataCollector().getJoins( assocClass ); if ( associatedClass == null ) { throw new MappingException( @@ -1141,7 +1173,18 @@ public abstract class CollectionBinder { LOG.debugf( "Mapping collection: %s -> %s", collection.getRole(), collection.getCollectionTable().getName() ); } bindFilters( false ); - bindCollectionSecondPass( collection, null, fkJoinColumns, cascadeDeleteEnabled, property, propertyHolder, buildingContext ); + handleWhere( false ); + + bindCollectionSecondPass( + collection, + null, + fkJoinColumns, + cascadeDeleteEnabled, + property, + propertyHolder, + buildingContext + ); + if ( !collection.isInverse() && !collection.getKey().isNullable() ) { // for non-inverse one-to-many, with a not-null fk, add a backref! @@ -1158,6 +1201,14 @@ public abstract class CollectionBinder { } } + private void handleJpaOrderBy(Collection collection, PersistentClass associatedClass) { + if ( jpaOrderBy != null ) { + final String orderByFragment = buildOrderByClauseFromHql( jpaOrderBy.value(), associatedClass ); + if ( StringHelper.isNotEmpty( orderByFragment ) ) { + collection.setOrderBy( orderByFragment ); + } + } + } private void bindFilters(boolean hasAssociationTable) { Filter simpleFilter = property.getAnnotation( Filter.class ); @@ -1165,58 +1216,48 @@ public abstract class CollectionBinder { //test incompatible choices //if ( StringHelper.isNotEmpty( where ) ) collection.setWhere( where ); if ( simpleFilter != null ) { - if ( hasAssociationTable ) { - collection.addManyToManyFilter(simpleFilter.name(), getCondition(simpleFilter), simpleFilter.deduceAliasInjectionPoints(), - toAliasTableMap(simpleFilter.aliases()), toAliasEntityMap(simpleFilter.aliases())); - } - else { - collection.addFilter(simpleFilter.name(), getCondition(simpleFilter), simpleFilter.deduceAliasInjectionPoints(), - toAliasTableMap(simpleFilter.aliases()), toAliasEntityMap(simpleFilter.aliases())); - } + addFilter( hasAssociationTable, simpleFilter ); } Filters filters = getOverridableAnnotation( property, Filters.class, buildingContext ); if ( filters != null ) { - for (Filter filter : filters.value()) { - if ( hasAssociationTable ) { - collection.addManyToManyFilter( filter.name(), getCondition(filter), filter.deduceAliasInjectionPoints(), - toAliasTableMap(filter.aliases()), toAliasEntityMap(filter.aliases())); - } - else { - collection.addFilter(filter.name(), getCondition(filter), filter.deduceAliasInjectionPoints(), - toAliasTableMap(filter.aliases()), toAliasEntityMap(filter.aliases())); - } + for ( Filter filter : filters.value() ) { + addFilter( hasAssociationTable, filter ); } } FilterJoinTable simpleFilterJoinTable = property.getAnnotation( FilterJoinTable.class ); if ( simpleFilterJoinTable != null ) { - if ( hasAssociationTable ) { - collection.addFilter(simpleFilterJoinTable.name(), simpleFilterJoinTable.condition(), - simpleFilterJoinTable.deduceAliasInjectionPoints(), - toAliasTableMap(simpleFilterJoinTable.aliases()), toAliasEntityMap(simpleFilterJoinTable.aliases())); - } - else { - throw new AnnotationException( - "Illegal use of @FilterJoinTable on an association without join table: " - + StringHelper.qualify( propertyHolder.getPath(), propertyName ) - ); - } + addFilter( hasAssociationTable, simpleFilterJoinTable ); } FilterJoinTables filterJoinTables = property.getAnnotation( FilterJoinTables.class ); if ( filterJoinTables != null ) { - for (FilterJoinTable filter : filterJoinTables.value()) { - if ( hasAssociationTable ) { - collection.addFilter(filter.name(), filter.condition(), - filter.deduceAliasInjectionPoints(), - toAliasTableMap(filter.aliases()), toAliasEntityMap(filter.aliases())); - } - else { - throw new AnnotationException( - "Illegal use of @FilterJoinTable on an association without join table: " - + StringHelper.qualify( propertyHolder.getPath(), propertyName ) - ); - } + for ( FilterJoinTable filter : filterJoinTables.value() ) { + addFilter( hasAssociationTable, filter ); } } + } + + private void addFilter(boolean hasAssociationTable, Filter filter) { + if (hasAssociationTable) { + collection.addManyToManyFilter( + filter.name(), + getCondition(filter), + filter.deduceAliasInjectionPoints(), + toAliasTableMap( filter.aliases() ), + toAliasEntityMap( filter.aliases() ) + ); + } + else { + collection.addFilter( + filter.name(), + getCondition(filter), + filter.deduceAliasInjectionPoints(), + toAliasTableMap( filter.aliases() ), + toAliasEntityMap( filter.aliases() ) + ); + } + } + + private void handleWhere(boolean hasAssociationTable) { final boolean useEntityWhereClauseForCollections = ConfigurationHelper.getBoolean( AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS, @@ -1250,7 +1291,7 @@ public abstract class CollectionBinder { whereOnClassClause, whereOnCollectionClause ); - if ( hasAssociationTable ) { + if (hasAssociationTable) { // A many-to-many association has an association (join) table // Collection#setManytoManyWhere is used to set the "where" clause that applies to // to the many-to-many associated entity table (not the join table). @@ -1266,7 +1307,7 @@ public abstract class CollectionBinder { WhereJoinTable whereJoinTable = property.getAnnotation( WhereJoinTable.class ); String whereJoinTableClause = whereJoinTable == null ? null : whereJoinTable.clause(); if ( StringHelper.isNotEmpty( whereJoinTableClause ) ) { - if ( hasAssociationTable ) { + if (hasAssociationTable) { // This is a many-to-many association. // Collection#setWhere is used to set the "where" clause that applies to the collection table // (which is the join table for a many-to-many association). @@ -1279,15 +1320,24 @@ public abstract class CollectionBinder { ); } } -// This cannot happen in annotations since the second fetch is hardcoded to join -// if ( ( ! collection.getManyToManyFilterMap().isEmpty() || collection.getManyToManyWhere() != null ) && -// collection.getFetchMode() == FetchMode.JOIN && -// collection.getElement().getFetchMode() != FetchMode.JOIN ) { -// throw new MappingException( -// "association with join table defining filter or where without join fetching " + -// "not valid within collection using join fetching [" + collection.getRole() + "]" -// ); -// } + } + + private void addFilter(boolean hasAssociationTable, FilterJoinTable filter) { + if (hasAssociationTable) { + collection.addFilter( + filter.name(), + filter.condition(), + filter.deduceAliasInjectionPoints(), + toAliasTableMap( filter.aliases() ), + toAliasEntityMap( filter.aliases() ) + ); + } + else { + throw new AnnotationException( + "Illegal use of @FilterJoinTable on an association without join table: " + + StringHelper.qualify( propertyHolder.getPath(), propertyName ) + ); + } } private String getCondition(Filter filter) { @@ -1298,7 +1348,7 @@ public abstract class CollectionBinder { } private String getCondition(String cond, String name) { - if ( BinderHelper.isEmptyAnnotationValue( cond ) ) { + if ( isEmptyAnnotationValue( cond ) ) { cond = buildingContext.getMetadataCollector().getFilterDefinition( name ).getDefaultFilterCondition(); if ( StringHelper.isEmpty( cond ) ) { throw new AnnotationException( @@ -1312,7 +1362,7 @@ public abstract class CollectionBinder { public void setCache(Cache cacheAnn) { if ( cacheAnn != null ) { - cacheRegionName = BinderHelper.isEmptyAnnotationValue( cacheAnn.region() ) ? null : cacheAnn.region(); + cacheRegionName = isEmptyAnnotationValue( cacheAnn.region() ) ? null : cacheAnn.region(); cacheConcurrencyStrategy = EntityBinder.getCacheConcurrencyStrategy( cacheAnn.usage() ); } else { @@ -1392,42 +1442,41 @@ public abstract class CollectionBinder { XProperty property, PropertyHolder propertyHolder, MetadataBuildingContext buildingContext) { - //binding key reference using column - KeyValue keyVal; + //give a chance to override the referenced property name //has to do that here because the referencedProperty creation happens in a FKSecondPass for Many to one yuk! if ( joinColumns.length > 0 && StringHelper.isNotEmpty( joinColumns[0].getMappedBy() ) ) { String entityName = joinColumns[0].getManyToManyOwnerSideEntityName() != null ? "inverse__" + joinColumns[0].getManyToManyOwnerSideEntityName() : joinColumns[0].getPropertyHolder().getEntityName(); - String propRef = buildingContext.getMetadataCollector().getPropertyReferencedAssociation( + InFlightMetadataCollector metadataCollector = buildingContext.getMetadataCollector(); + String propRef = metadataCollector.getPropertyReferencedAssociation( entityName, joinColumns[0].getMappedBy() ); if ( propRef != null ) { collValue.setReferencedPropertyName( propRef ); - buildingContext.getMetadataCollector().addPropertyReference( collValue.getOwnerEntityName(), propRef ); + metadataCollector.addPropertyReference( collValue.getOwnerEntityName(), propRef ); } } + String propRef = collValue.getReferencedPropertyName(); - if ( propRef == null ) { - keyVal = collValue.getOwner().getIdentifier(); - } - else { - keyVal = (KeyValue) collValue.getOwner() - .getReferencedProperty( propRef ) - .getValue(); - } + //binding key reference using column + KeyValue keyVal = propRef == null + ? collValue.getOwner().getIdentifier() + : (KeyValue) collValue.getOwner().getReferencedProperty(propRef).getValue(); + DependantValue key = new DependantValue( buildingContext, collValue.getCollectionTable(), keyVal ); key.setTypeName( null ); - AnnotatedColumn.checkPropertyConsistency( joinColumns, collValue.getOwnerEntityName() ); + checkPropertyConsistency( joinColumns, collValue.getOwnerEntityName() ); key.setNullable( joinColumns.length == 0 || joinColumns[0].isNullable() ); key.setUpdateable( joinColumns.length == 0 || joinColumns[0].isUpdatable() ); key.setCascadeDeleteEnabled( cascadeDeleteEnabled ); collValue.setKey( key ); + if ( property != null ) { final ForeignKey fk = property.getAnnotation( ForeignKey.class ); - if ( fk != null && !BinderHelper.isEmptyAnnotationValue( fk.name() ) ) { + if ( fk != null && !isEmptyAnnotationValue( fk.name() ) ) { key.setForeignKeyName( fk.name() ); } else { @@ -1531,6 +1580,7 @@ public abstract class CollectionBinder { XProperty property, PropertyHolder parentPropertyHolder, MetadataBuildingContext buildingContext) throws MappingException { + if ( property == null ) { throw new IllegalArgumentException( "null was passed for argument property" ); } @@ -1539,363 +1589,85 @@ public abstract class CollectionBinder { final String hqlOrderBy = extractHqlOrderBy( jpaOrderBy ); boolean isCollectionOfEntities = collectionEntity != null; - ManyToAny anyAnn = property.getAnnotation( ManyToAny.class ); - if ( LOG.isDebugEnabled() ) { - String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); - if ( isCollectionOfEntities && unique ) { - LOG.debugf("Binding a OneToMany: %s through an association table", path); - } - else if (isCollectionOfEntities) { - LOG.debugf("Binding a ManyToMany: %s", path); - } - else if (anyAnn != null) { - LOG.debugf("Binding a ManyToAny: %s", path); - } - else { - LOG.debugf("Binding a collection of element: %s", path); - } - } - //check for user error - if ( !isCollectionOfEntities ) { - if ( property.isAnnotationPresent( ManyToMany.class ) || property.isAnnotationPresent( OneToMany.class ) ) { - String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); - throw new AnnotationException( - "Use of @OneToMany or @ManyToMany targeting an unmapped class: " + path + "[" + collType + "]" - ); - } - else if ( anyAnn != null ) { - if ( parentPropertyHolder.getJoinTable( property ) == null ) { - String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); - throw new AnnotationException( - "@JoinTable is mandatory when @ManyToAny is used: " + path - ); - } - } - else { - JoinTable joinTableAnn = parentPropertyHolder.getJoinTable( property ); - if ( joinTableAnn != null && joinTableAnn.inverseJoinColumns().length > 0 ) { - String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); - throw new AnnotationException( - "Use of @JoinTable.inverseJoinColumns targeting an unmapped class: " + path + "[" + collType + "]" - ); - } - } - } + boolean isManyToAny = property.isAnnotationPresent( ManyToAny.class ); - boolean mappedBy = !BinderHelper.isEmptyAnnotationValue( joinColumns[0].getMappedBy() ); - if ( mappedBy ) { - if ( !isCollectionOfEntities ) { - throw new AnnotationException( - "Collection of elements must not have mappedBy or association reference an unmapped entity: " + - collValue.getOwnerEntityName() + - "." + - joinColumns[0].getPropertyName() - ); - } - Property otherSideProperty; - try { - otherSideProperty = collectionEntity.getRecursiveProperty( joinColumns[0].getMappedBy() ); - } - catch (MappingException e) { - throw new AnnotationException( - "mappedBy references an unknown target entity property: " - + collType + "." + joinColumns[0].getMappedBy() + " in " - + collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName() - ); - } - Table table; - if ( otherSideProperty.getValue() instanceof Collection ) { - //this is a collection on the other side - table = ( (Collection) otherSideProperty.getValue() ).getCollectionTable(); - } - else { - //This is a ToOne with a @JoinTable or a regular property - table = otherSideProperty.getValue().getTable(); - } - collValue.setCollectionTable( table ); - String entityName = collectionEntity.getEntityName(); - for (AnnotatedJoinColumn column : joinColumns) { - //column.setDefaultColumnHeader( joinColumns[0].getMappedBy() ); //seems not to be used, make sense - column.setManyToManyOwnerSideEntityName( entityName ); - } + logManyToManySecondPass( collValue, joinColumns, unique, isCollectionOfEntities, isManyToAny ); + //check for user error + detectManyToManyProblems( + collValue, + joinColumns, + collType, + property, + parentPropertyHolder, + isCollectionOfEntities, + isManyToAny + ); + + if ( !isEmptyAnnotationValue( joinColumns[0].getMappedBy() ) ) { + handleUnownedManyToMany( + collValue, + joinColumns, + collType, + collectionEntity, + isCollectionOfEntities + ); } else { - //TODO: only for implicit columns? - //FIXME NamingStrategy - for (AnnotatedJoinColumn column : joinColumns) { - String mappedByProperty = buildingContext.getMetadataCollector().getFromMappedBy( - collValue.getOwnerEntityName(), column.getPropertyName() - ); - Table ownerTable = collValue.getOwner().getTable(); - column.setMappedBy( - collValue.getOwner().getEntityName(), - collValue.getOwner().getJpaEntityName(), - buildingContext.getMetadataCollector().getLogicalTableName( ownerTable ), - mappedByProperty - ); -// String header = ( mappedByProperty == null ) ? mappings.getLogicalTableName( ownerTable ) : mappedByProperty; -// column.setDefaultColumnHeader( header ); - } - if ( StringHelper.isEmpty( associationTableBinder.getName() ) ) { - //default value - associationTableBinder.setDefaultName( - collValue.getOwner().getClassName(), - collValue.getOwner().getEntityName(), - collValue.getOwner().getJpaEntityName(), - buildingContext.getMetadataCollector().getLogicalTableName( collValue.getOwner().getTable() ), - collectionEntity != null ? collectionEntity.getClassName() : null, - collectionEntity != null ? collectionEntity.getEntityName() : null, - collectionEntity != null ? collectionEntity.getJpaEntityName() : null, - collectionEntity != null ? buildingContext.getMetadataCollector().getLogicalTableName( - collectionEntity.getTable() - ) : null, - joinColumns[0].getPropertyName() - ); - } - associationTableBinder.setJPA2ElementCollection( !isCollectionOfEntities && property.isAnnotationPresent( ElementCollection.class )); - collValue.setCollectionTable( associationTableBinder.bind() ); + handleOwnedManyToMany( + collValue, + joinColumns, + associationTableBinder, + property, + buildingContext, + collectionEntity, + isCollectionOfEntities + ); } bindFilters( isCollectionOfEntities ); - bindCollectionSecondPass( collValue, collectionEntity, joinColumns, cascadeDeleteEnabled, property, propertyHolder, buildingContext ); + handleWhere( isCollectionOfEntities ); + + bindCollectionSecondPass( + collValue, + collectionEntity, + joinColumns, + cascadeDeleteEnabled, + property, + propertyHolder, + buildingContext + ); ManyToOne element = null; if ( isCollectionOfEntities ) { - element = new ManyToOne( buildingContext, collValue.getCollectionTable() ); - collValue.setElement( element ); - element.setReferencedEntityName( collType.getName() ); - //element.setFetchMode( fetchMode ); - //element.setLazy( fetchMode != FetchMode.JOIN ); - //make the second join non lazy - element.setFetchMode( FetchMode.JOIN ); - element.setLazy( false ); - element.setIgnoreNotFound( ignoreNotFound ); - // as per 11.1.38 of JPA 2.0 spec, default to primary key if no column is specified by @OrderBy. - if ( hqlOrderBy != null ) { - collValue.setManyToManyOrdering( - buildOrderByClauseFromHql( hqlOrderBy, collectionEntity) - ); - } - - final ForeignKey fk = property.getAnnotation( ForeignKey.class ); - if ( fk != null && !BinderHelper.isEmptyAnnotationValue( fk.name() ) ) { - element.setForeignKeyName( fk.name() ); - } - else { - final JoinTable joinTableAnn = property.getAnnotation( JoinTable.class ); - if ( joinTableAnn != null ) { - String foreignKeyName = joinTableAnn.inverseForeignKey().name(); - String foreignKeyDefinition = joinTableAnn.inverseForeignKey().foreignKeyDefinition(); - if ( joinTableAnn.inverseJoinColumns().length != 0 ) { - final JoinColumn joinColumnAnn = joinTableAnn.inverseJoinColumns()[0]; - if ( foreignKeyName != null && foreignKeyName.isEmpty() ) { - foreignKeyName = joinColumnAnn.foreignKey().name(); - foreignKeyDefinition = joinColumnAnn.foreignKey().foreignKeyDefinition(); - } - } - if ( joinTableAnn.inverseForeignKey().value() == ConstraintMode.NO_CONSTRAINT - || joinTableAnn.inverseForeignKey().value() == ConstraintMode.PROVIDER_DEFAULT && buildingContext.getBuildingOptions().isNoConstraintByDefault() ) { - element.disableForeignKey(); - } - else { - element.setForeignKeyName( StringHelper.nullIfEmpty( foreignKeyName ) ); - element.setForeignKeyDefinition( StringHelper.nullIfEmpty( foreignKeyDefinition ) ); - } - } - } - } - else if ( anyAnn != null ) { - //@ManyToAny - //Make sure that collTyp is never used during the @ManyToAny branch: it will be set to void.class - final PropertyData inferredData = new PropertyInferredData( - null, + element = handleCollectionOfEntities( + collValue, + collType, + ignoreNotFound, property, - "unsupported", - buildingContext.getBootstrapContext().getReflectionManager() + buildingContext, + collectionEntity, + hqlOrderBy ); - - XProperty prop = inferredData.getProperty(); - final jakarta.persistence.Column discriminatorColumnAnn = prop.getAnnotation( jakarta.persistence.Column.class ); - final Formula discriminatorFormulaAnn = getOverridableAnnotation( prop, Formula.class, buildingContext ); - - //override the table - for (AnnotatedColumn column : inverseJoinColumns) { - column.setTable( collValue.getCollectionTable() ); - } - - final Any any = BinderHelper.buildAnyValue( - discriminatorColumnAnn, - discriminatorFormulaAnn, + } + else if ( isManyToAny ) { + handleManyToAny( + collValue, inverseJoinColumns, - inferredData, cascadeDeleteEnabled, - anyAnn.fetch() == FetchType.LAZY, - Nullability.NO_CONSTRAINT, - propertyHolder, - new EntityBinder(), - true, + property, buildingContext ); - collValue.setElement( any ); } else { - XClass elementClass; - AnnotatedClassType classType; - - CollectionPropertyHolder holder; - if ( BinderHelper.PRIMITIVE_NAMES.contains( collType.getName() ) ) { - classType = AnnotatedClassType.NONE; - elementClass = null; - - holder = PropertyHolderBuilder.buildPropertyHolder( - collValue, - collValue.getRole(), - null, - property, - parentPropertyHolder, - buildingContext - ); - } - else { - elementClass = collType; - classType = buildingContext.getMetadataCollector().getClassType( elementClass ); - - holder = PropertyHolderBuilder.buildPropertyHolder( - collValue, - collValue.getRole(), - elementClass, - property, - parentPropertyHolder, - buildingContext - ); - - // 'parentPropertyHolder' is the PropertyHolder for the owner of the collection - // 'holder' is the CollectionPropertyHolder. - // 'property' is the collection XProperty - parentPropertyHolder.startingProperty( property ); - - //force in case of attribute override - boolean attributeOverride = property.isAnnotationPresent( AttributeOverride.class ) - || property.isAnnotationPresent( AttributeOverrides.class ); - // todo : force in the case of Convert annotation(s) with embedded paths (beyond key/value prefixes)? - if ( isEmbedded || attributeOverride ) { - classType = AnnotatedClassType.EMBEDDABLE; - } - } - - if ( AnnotatedClassType.EMBEDDABLE.equals( classType ) ) { - holder.prepare( property ); - - EntityBinder entityBinder = new EntityBinder(); - PersistentClass owner = collValue.getOwner(); - - final AccessType baseAccessType; - final Access accessAnn = property.getAnnotation( Access.class ); - if ( accessAnn != null ) { - // the attribute is locally annotated with `@Access`, use that - baseAccessType = accessAnn.value() == PROPERTY - ? AccessType.PROPERTY - : AccessType.FIELD; - } - else if ( owner.getIdentifierProperty() != null ) { - // use the access for the owning entity's id attribute, if one - baseAccessType = owner.getIdentifierProperty().getPropertyAccessorName().equals( "property" ) - ? AccessType.PROPERTY - : AccessType.FIELD; - } - else if ( owner.getIdentifierMapper() != null && owner.getIdentifierMapper().getPropertySpan() > 0 ) { - // use the access for the owning entity's "id mapper", if one - Property prop = owner.getIdentifierMapper().getProperties().get(0); - baseAccessType = prop.getPropertyAccessorName().equals( "property" ) - ? AccessType.PROPERTY - : AccessType.FIELD; - } - else { - // otherwise... - throw new AssertionFailure( "Unable to guess collection property accessor name" ); - } - - PropertyData inferredData; - if ( isMap() ) { - //"value" is the JPA 2 prefix for map values (used to be "element") - if ( isHibernateExtensionMapping() ) { - inferredData = new PropertyPreloadedData( AccessType.PROPERTY, "element", elementClass ); - } - else { - inferredData = new PropertyPreloadedData( AccessType.PROPERTY, "value", elementClass ); - } - } - else { - if ( isHibernateExtensionMapping() ) { - inferredData = new PropertyPreloadedData( AccessType.PROPERTY, "element", elementClass ); - } - else { - //"collection&&element" is not a valid property name => placeholder - inferredData = new PropertyPreloadedData( AccessType.PROPERTY, "collection&&element", elementClass ); - } - } - - //TODO be smart with isNullable - boolean isNullable = true; - Component component = AnnotationBinder.fillComponent( - holder, - inferredData, - baseAccessType, - isNullable, - entityBinder, - false, - false, - true, - resolveCustomInstantiator( property, elementClass, buildingContext ), - buildingContext, - inheritanceStatePerClass - ); - - collValue.setElement( component ); - - if ( StringHelper.isNotEmpty( hqlOrderBy ) ) { - String orderBy = adjustUserSuppliedValueCollectionOrderingFragment( hqlOrderBy ); - if ( orderBy != null ) { - collValue.setOrderBy( orderBy ); - } - } - } - else { - holder.prepare( property ); - - final BasicValueBinder elementBinder = new BasicValueBinder( BasicValueBinder.Kind.COLLECTION_ELEMENT, buildingContext ); - elementBinder.setReturnedClassName( collType.getName() ); - if ( elementColumns == null || elementColumns.length == 0 ) { - elementColumns = new AnnotatedColumn[1]; - AnnotatedColumn column = new AnnotatedColumn(); - column.setImplicit( false ); - //not following the spec but more clean - column.setNullable( true ); - column.setLogicalColumnName( Collection.DEFAULT_ELEMENT_COLUMN_NAME ); - //TODO create an EMPTY_JOINS collection - column.setJoins( new HashMap<>() ); - column.setBuildingContext( buildingContext ); - column.bind(); - elementColumns[0] = column; - } - //override the table - for (AnnotatedColumn column : elementColumns) { - column.setTable( collValue.getCollectionTable() ); - } - elementBinder.setColumns( elementColumns ); - elementBinder.setType( - property, - elementClass, - collValue.getOwnerEntityName(), - holder.resolveElementAttributeConverterDescriptor( property, elementClass ) - ); - elementBinder.setPersistentClassName( propertyHolder.getEntityName() ); - elementBinder.setAccessType( accessType ); - collValue.setElement( elementBinder.make() ); - String orderBy = adjustUserSuppliedValueCollectionOrderingFragment( hqlOrderBy ); - if ( orderBy != null ) { - collValue.setOrderBy( orderBy ); - } - } + handleElementCollection( + collValue, + elementColumns, + isEmbedded, + collType, + property, + parentPropertyHolder, + buildingContext, + hqlOrderBy + ); } checkFilterConditions( collValue ); @@ -1907,6 +1679,402 @@ public abstract class CollectionBinder { } + private void handleElementCollection(Collection collValue, AnnotatedColumn[] elementColumns, boolean isEmbedded, XClass collType, XProperty property, PropertyHolder parentPropertyHolder, MetadataBuildingContext buildingContext, String hqlOrderBy) { + XClass elementClass; + AnnotatedClassType classType; + + CollectionPropertyHolder holder; + if ( BinderHelper.PRIMITIVE_NAMES.contains( collType.getName() ) ) { + classType = AnnotatedClassType.NONE; + elementClass = null; + + holder = PropertyHolderBuilder.buildPropertyHolder( + collValue, + collValue.getRole(), + null, + property, + parentPropertyHolder, + buildingContext + ); + } + else { + elementClass = collType; + classType = buildingContext.getMetadataCollector().getClassType( elementClass ); + + holder = PropertyHolderBuilder.buildPropertyHolder( + collValue, + collValue.getRole(), + elementClass, + property, + parentPropertyHolder, + buildingContext + ); + + // 'parentPropertyHolder' is the PropertyHolder for the owner of the collection + // 'holder' is the CollectionPropertyHolder. + // 'property' is the collection XProperty + parentPropertyHolder.startingProperty(property); + + //force in case of attribute override + boolean attributeOverride = property.isAnnotationPresent( AttributeOverride.class ) + || property.isAnnotationPresent( AttributeOverrides.class ); + // todo : force in the case of Convert annotation(s) with embedded paths (beyond key/value prefixes)? + if ( isEmbedded || attributeOverride ) { + classType = AnnotatedClassType.EMBEDDABLE; + } + } + + if ( AnnotatedClassType.EMBEDDABLE == classType ) { + holder.prepare(property); + + EntityBinder entityBinder = new EntityBinder(); + PersistentClass owner = collValue.getOwner(); + + final AccessType baseAccessType; + final Access accessAnn = property.getAnnotation( Access.class ); + if ( accessAnn != null ) { + // the attribute is locally annotated with `@Access`, use that + baseAccessType = accessAnn.value() == PROPERTY + ? AccessType.PROPERTY + : AccessType.FIELD; + } + else if ( owner.getIdentifierProperty() != null ) { + // use the access for the owning entity's id attribute, if one + baseAccessType = owner.getIdentifierProperty().getPropertyAccessorName().equals( "property" ) + ? AccessType.PROPERTY + : AccessType.FIELD; + } + else if ( owner.getIdentifierMapper() != null && owner.getIdentifierMapper().getPropertySpan() > 0 ) { + // use the access for the owning entity's "id mapper", if one + Property prop = owner.getIdentifierMapper().getProperties().get(0); + baseAccessType = prop.getPropertyAccessorName().equals( "property" ) + ? AccessType.PROPERTY + : AccessType.FIELD; + } + else { + // otherwise... + throw new AssertionFailure( "Unable to guess collection property accessor name" ); + } + + //TODO be smart with isNullable + Component component = fillComponent( + holder, + getSpecialMembers( elementClass ), + baseAccessType, + true, + entityBinder, + false, + false, + true, + resolveCustomInstantiator(property, elementClass, buildingContext), + buildingContext, + inheritanceStatePerClass + ); + + collValue.setElement( component ); + + if ( StringHelper.isNotEmpty(hqlOrderBy) ) { + String orderBy = adjustUserSuppliedValueCollectionOrderingFragment(hqlOrderBy); + if ( orderBy != null ) { + collValue.setOrderBy( orderBy ); + } + } + } + else { + holder.prepare(property); + + final BasicValueBinder elementBinder = + new BasicValueBinder<>( BasicValueBinder.Kind.COLLECTION_ELEMENT, buildingContext); + elementBinder.setReturnedClassName( collType.getName() ); + if ( elementColumns == null || elementColumns.length == 0 ) { + elementColumns = new AnnotatedColumn[1]; + AnnotatedColumn column = new AnnotatedColumn(); + column.setImplicit( false ); + //not following the spec but more clean + column.setNullable( true ); + column.setLogicalColumnName( Collection.DEFAULT_ELEMENT_COLUMN_NAME ); + //TODO create an EMPTY_JOINS collection + column.setJoins( new HashMap<>() ); + column.setBuildingContext(buildingContext); + column.bind(); + elementColumns[0] = column; + } + //override the table + for (AnnotatedColumn column : elementColumns) { + column.setTable( collValue.getCollectionTable() ); + } + elementBinder.setColumns(elementColumns); + elementBinder.setType( + property, + elementClass, + collValue.getOwnerEntityName(), + holder.resolveElementAttributeConverterDescriptor(property, elementClass ) + ); + elementBinder.setPersistentClassName( propertyHolder.getEntityName() ); + elementBinder.setAccessType( accessType ); + collValue.setElement( elementBinder.make() ); + String orderBy = adjustUserSuppliedValueCollectionOrderingFragment(hqlOrderBy); + if ( orderBy != null ) { + collValue.setOrderBy( orderBy ); + } + } + } + + private ManyToOne handleCollectionOfEntities(Collection collValue, XClass collType, boolean ignoreNotFound, XProperty property, MetadataBuildingContext buildingContext, PersistentClass collectionEntity, String hqlOrderBy) { + ManyToOne element; + element = new ManyToOne(buildingContext, collValue.getCollectionTable() ); + collValue.setElement( element ); + element.setReferencedEntityName( collType.getName() ); + //element.setFetchMode( fetchMode ); + //element.setLazy( fetchMode != FetchMode.JOIN ); + //make the second join non lazy + element.setFetchMode( FetchMode.JOIN ); + element.setLazy( false ); + element.setIgnoreNotFound(ignoreNotFound); + // as per 11.1.38 of JPA 2.0 spec, default to primary key if no column is specified by @OrderBy. + if ( hqlOrderBy != null ) { + collValue.setManyToManyOrdering( + buildOrderByClauseFromHql(hqlOrderBy, collectionEntity) + ); + } + + final ForeignKey fk = property.getAnnotation( ForeignKey.class ); + if ( fk != null && !isEmptyAnnotationValue( fk.name() ) ) { + element.setForeignKeyName( fk.name() ); + } + else { + final JoinTable joinTableAnn = property.getAnnotation( JoinTable.class ); + if ( joinTableAnn != null ) { + String foreignKeyName = joinTableAnn.inverseForeignKey().name(); + String foreignKeyDefinition = joinTableAnn.inverseForeignKey().foreignKeyDefinition(); + if ( joinTableAnn.inverseJoinColumns().length != 0 ) { + final JoinColumn joinColumnAnn = joinTableAnn.inverseJoinColumns()[0]; + if ( foreignKeyName != null && foreignKeyName.isEmpty() ) { + foreignKeyName = joinColumnAnn.foreignKey().name(); + foreignKeyDefinition = joinColumnAnn.foreignKey().foreignKeyDefinition(); + } + } + if ( joinTableAnn.inverseForeignKey().value() == ConstraintMode.NO_CONSTRAINT + || joinTableAnn.inverseForeignKey().value() == ConstraintMode.PROVIDER_DEFAULT + && buildingContext.getBuildingOptions().isNoConstraintByDefault() ) { + element.disableForeignKey(); + } + else { + element.setForeignKeyName( StringHelper.nullIfEmpty( foreignKeyName ) ); + element.setForeignKeyDefinition( StringHelper.nullIfEmpty( foreignKeyDefinition ) ); + } + } + } + return element; + } + + private void handleManyToAny(Collection collValue, AnnotatedJoinColumn[] inverseJoinColumns, boolean cascadeDeleteEnabled, XProperty property, MetadataBuildingContext buildingContext) { + //@ManyToAny + //Make sure that collTyp is never used during the @ManyToAny branch: it will be set to void.class + final PropertyData inferredData = new PropertyInferredData( + null, + property, + "unsupported", + buildingContext.getBootstrapContext().getReflectionManager() + ); + + XProperty prop = inferredData.getProperty(); + final jakarta.persistence.Column discriminatorColumnAnn = prop.getAnnotation( jakarta.persistence.Column.class ); + final Formula discriminatorFormulaAnn = getOverridableAnnotation( prop, Formula.class, buildingContext); + + //override the table + for (AnnotatedColumn column : inverseJoinColumns) { + column.setTable( collValue.getCollectionTable() ); + } + + ManyToAny anyAnn = property.getAnnotation( ManyToAny.class ); + final Any any = BinderHelper.buildAnyValue( + discriminatorColumnAnn, + discriminatorFormulaAnn, + inverseJoinColumns, + inferredData, + cascadeDeleteEnabled, + anyAnn.fetch() == FetchType.LAZY, + Nullability.NO_CONSTRAINT, + propertyHolder, + new EntityBinder(), + true, + buildingContext + ); + collValue.setElement( any ); + } + + private PropertyData getSpecialMembers(XClass elementClass) { + if ( isMap() ) { + //"value" is the JPA 2 prefix for map values (used to be "element") + if ( isHibernateExtensionMapping() ) { + return new PropertyPreloadedData( AccessType.PROPERTY, "element", elementClass); + } + else { + return new PropertyPreloadedData( AccessType.PROPERTY, "value", elementClass); + } + } + else { + if ( isHibernateExtensionMapping() ) { + return new PropertyPreloadedData( AccessType.PROPERTY, "element", elementClass); + } + else { + //"collection&&element" is not a valid property name => placeholder + return new PropertyPreloadedData( AccessType.PROPERTY, "collection&&element", elementClass); + } + } + } + + private void handleOwnedManyToMany( + Collection collValue, + AnnotatedJoinColumn[] joinColumns, + TableBinder associationTableBinder, + XProperty property, + MetadataBuildingContext buildingContext, + PersistentClass collectionEntity, + boolean isCollectionOfEntities) { + //TODO: only for implicit columns? + //FIXME NamingStrategy + for (AnnotatedJoinColumn column : joinColumns) { + String mappedByProperty = buildingContext.getMetadataCollector().getFromMappedBy( + collValue.getOwnerEntityName(), column.getPropertyName() + ); + Table ownerTable = collValue.getOwner().getTable(); + column.setMappedBy( + collValue.getOwner().getEntityName(), + collValue.getOwner().getJpaEntityName(), + buildingContext.getMetadataCollector().getLogicalTableName( ownerTable ), + mappedByProperty + ); +// String header = ( mappedByProperty == null ) ? mappings.getLogicalTableName( ownerTable ) : mappedByProperty; +// column.setDefaultColumnHeader( header ); + } + if ( StringHelper.isEmpty( associationTableBinder.getName() ) ) { + //default value + associationTableBinder.setDefaultName( + collValue.getOwner().getClassName(), + collValue.getOwner().getEntityName(), + collValue.getOwner().getJpaEntityName(), + buildingContext.getMetadataCollector().getLogicalTableName( collValue.getOwner().getTable() ), + collectionEntity != null ? collectionEntity.getClassName() : null, + collectionEntity != null ? collectionEntity.getEntityName() : null, + collectionEntity != null ? collectionEntity.getJpaEntityName() : null, + collectionEntity != null ? buildingContext.getMetadataCollector().getLogicalTableName( + collectionEntity.getTable() + ) : null, + joinColumns[0].getPropertyName() + ); + } + associationTableBinder.setJPA2ElementCollection( + !isCollectionOfEntities && property.isAnnotationPresent(ElementCollection.class) + ); + collValue.setCollectionTable( associationTableBinder.bind() ); + } + + private void handleUnownedManyToMany( + Collection collValue, + AnnotatedJoinColumn[] joinColumns, + XClass collType, + PersistentClass collectionEntity, + boolean isCollectionOfEntities) { + + if ( !isCollectionOfEntities) { + throw new AnnotationException( + "Collection of elements must not have mappedBy or association reference an unmapped entity: " + + collValue.getOwnerEntityName() + + "." + + joinColumns[0].getPropertyName() + ); + } + + Property otherSideProperty; + try { + otherSideProperty = collectionEntity.getRecursiveProperty( joinColumns[0].getMappedBy() ); + } + catch (MappingException e) { + throw new AnnotationException( + "mappedBy references an unknown target entity property: " + + collType + "." + joinColumns[0].getMappedBy() + " in " + + collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName() + ); + } + Table table; + if ( otherSideProperty.getValue() instanceof Collection ) { + //this is a collection on the other side + table = ( (Collection) otherSideProperty.getValue() ).getCollectionTable(); + } + else { + //This is a ToOne with a @JoinTable or a regular property + table = otherSideProperty.getValue().getTable(); + } + collValue.setCollectionTable( table ); + String entityName = collectionEntity.getEntityName(); + for (AnnotatedJoinColumn column : joinColumns) { + //column.setDefaultColumnHeader( joinColumns[0].getMappedBy() ); //seems not to be used, make sense + column.setManyToManyOwnerSideEntityName( entityName ); + } + } + + private void detectManyToManyProblems( + Collection collValue, + AnnotatedJoinColumn[] joinColumns, + XClass collType, + XProperty property, + PropertyHolder parentPropertyHolder, + boolean isCollectionOfEntities, + boolean isManyToAny) { + + if ( !isCollectionOfEntities) { + if ( property.isAnnotationPresent( ManyToMany.class ) || property.isAnnotationPresent( OneToMany.class ) ) { + String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); + throw new AnnotationException( + "Use of @OneToMany or @ManyToMany targeting an unmapped class: " + path + "[" + collType + "]" + ); + } + else if (isManyToAny) { + if ( parentPropertyHolder.getJoinTable(property) == null ) { + String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); + throw new AnnotationException( + "@JoinTable is mandatory when @ManyToAny is used: " + path + ); + } + } + else { + JoinTable joinTableAnn = parentPropertyHolder.getJoinTable(property); + if ( joinTableAnn != null && joinTableAnn.inverseJoinColumns().length > 0 ) { + String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); + throw new AnnotationException( + "Use of @JoinTable.inverseJoinColumns targeting an unmapped class: " + path + "[" + collType + "]" + ); + } + } + } + } + + private void logManyToManySecondPass( + Collection collValue, + AnnotatedJoinColumn[] joinColumns, + boolean unique, + boolean isCollectionOfEntities, + boolean isManyToAny) { + + if ( LOG.isDebugEnabled() ) { + String path = collValue.getOwnerEntityName() + "." + joinColumns[0].getPropertyName(); + if ( isCollectionOfEntities && unique) { + LOG.debugf("Binding a OneToMany: %s through an association table", path); + } + else if (isCollectionOfEntities) { + LOG.debugf("Binding a ManyToMany: %s", path); + } + else if (isManyToAny) { + LOG.debugf("Binding a ManyToAny: %s", path); + } + else { + LOG.debugf("Binding a collection of element: %s", path); + } + } + } + private Class resolveCustomInstantiator( XProperty property, XClass propertyClass, @@ -1984,17 +2152,12 @@ public abstract class CollectionBinder { } private String safeCollectionRole() { - if ( propertyHolder != null ) { - return propertyHolder.getEntityName() + "." + propertyName; - } - else { - return ""; - } + return propertyHolder != null ? propertyHolder.getEntityName() + "." + propertyName : ""; } /** - * bind the inverse FK of a ManyToMany + * bind the inverse FK of a {@link ManyToMany}. * If we are in a mappedBy case, read the columns from the associated * collection element * Otherwise delegates to the usual algorithm diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/IdBagBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/IdBagBinder.java index e6b03fb516..4509057cbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/IdBagBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/IdBagBinder.java @@ -28,7 +28,6 @@ import org.hibernate.mapping.Collection; import org.hibernate.mapping.IdentifierBag; import org.hibernate.mapping.IdentifierCollection; import org.hibernate.mapping.PersistentClass; -import org.hibernate.mapping.SemanticsResolver; import org.hibernate.mapping.Table; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.usertype.UserCollectionType; @@ -49,7 +48,7 @@ public class IdBagBinder extends BagBinder { @Override protected boolean bindStarToManySecondPass( - Map persistentClasses, + Map persistentClasses, XClass collType, AnnotatedJoinColumn[] fkJoinColumns, AnnotatedJoinColumn[] keyColumns, @@ -62,8 +61,18 @@ public class IdBagBinder extends BagBinder { boolean ignoreNotFound, MetadataBuildingContext buildingContext) { boolean result = super.bindStarToManySecondPass( - persistentClasses, collType, fkJoinColumns, keyColumns, inverseColumns, elementColumns, isEmbedded, - property, unique, associationTableBinder, ignoreNotFound, getBuildingContext() + persistentClasses, + collType, + fkJoinColumns, + keyColumns, + inverseColumns, + elementColumns, + isEmbedded, + property, + unique, + associationTableBinder, + ignoreNotFound, + getBuildingContext() ); final CollectionId collectionIdAnn = property.getAnnotation( CollectionId.class ); @@ -82,10 +91,9 @@ public class IdBagBinder extends BagBinder { "id" ); - final AnnotatedColumn[] idColumns = AnnotatedColumn.buildColumnFromAnnotation( + final AnnotatedColumn[] idColumns = AnnotatedColumn.buildColumnsFromAnnotations( new Column[] { collectionIdAnn.column() }, null, - null, Nullability.FORCED_NOT_NULL, propertyHolder, propertyData, @@ -98,7 +106,7 @@ public class IdBagBinder extends BagBinder { idColumn.setNullable( false ); } - final BasicValueBinder valueBinder = new BasicValueBinder( BasicValueBinder.Kind.COLLECTION_ID, buildingContext ); + final BasicValueBinder valueBinder = new BasicValueBinder<>( BasicValueBinder.Kind.COLLECTION_ID, buildingContext ); final Table table = collection.getCollectionTable(); valueBinder.setTable( table ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entitynonentity/EntityNonEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entitynonentity/EntityNonEntityTest.java index d40ea2ac0e..1e188eeaf5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entitynonentity/EntityNonEntityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entitynonentity/EntityNonEntityTest.java @@ -24,7 +24,7 @@ import static org.junit.Assert.fail; */ public class EntityNonEntityTest extends BaseCoreFunctionalTestCase { @Test - public void testMix() throws Exception { + public void testMix() { GSM gsm = new GSM(); gsm.brand = "Sony"; gsm.frequency = 900; @@ -37,7 +37,7 @@ public class EntityNonEntityTest extends BaseCoreFunctionalTestCase { tx.commit(); s.clear(); tx = s.beginTransaction(); - gsm = (GSM) s.get( GSM.class, gsm.id ); + gsm = s.get( GSM.class, gsm.id ); assertEquals( "top mapped superclass", 2, gsm.number ); assertNull( "non entity between mapped superclass and entity", gsm.species ); assertTrue( "mapped superclass under entity", gsm.isNumeric ); diff --git a/hibernate-testing/src/test/java/org/hibernate/testing/annotations/BasicSessionFactoryScopeTests.java b/hibernate-testing/src/test/java/org/hibernate/testing/annotations/BasicSessionFactoryScopeTests.java index 77851c72f0..ac59bbb943 100644 --- a/hibernate-testing/src/test/java/org/hibernate/testing/annotations/BasicSessionFactoryScopeTests.java +++ b/hibernate-testing/src/test/java/org/hibernate/testing/annotations/BasicSessionFactoryScopeTests.java @@ -27,7 +27,7 @@ public class BasicSessionFactoryScopeTests { assertThat( scope.getSessionFactory(), notNullValue() ); // check we can use the SF to create Sessions scope.inTransaction( - (session) -> session.createQuery( "from AnEntity" ).list() + session -> session.createQuery( "from AnEntity" ).list() ); }