diff --git a/ci/jpa-3.1-tck.Jenkinsfile b/ci/jpa-3.1-tck.Jenkinsfile index 0c01b1ab9d55..8eaeced30a79 100644 --- a/ci/jpa-3.1-tck.Jenkinsfile +++ b/ci/jpa-3.1-tck.Jenkinsfile @@ -26,8 +26,8 @@ pipeline { } parameters { choice(name: 'IMAGE_JDK', choices: ['jdk11'], description: 'The JDK base image version to use for the TCK image.') - string(name: 'TCK_VERSION', defaultValue: '3.1.2', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') - string(name: 'TCK_SHA', defaultValue: '618a9fcdb0f897cda71227ed57d035ae1dc40fc392318809a734ffc6968e43ff', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') + string(name: 'TCK_VERSION', defaultValue: '3.1.6', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') + string(name: 'TCK_SHA', defaultValue: '790ca7a2a95ea098cfedafa2689c0d7a379fa62c74fed9505dd23191292f59fe', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') booleanParam(name: 'NO_SLEEP', defaultValue: true, description: 'Whether the NO_SLEEP patch should be applied to speed up the TCK execution') } stages { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java index 6abb62a1e371..9a0cf14aaff8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java @@ -11,6 +11,7 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.DiscriminatorType; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.persister.entity.JoinedSubclassEntityPersister; @@ -88,18 +89,23 @@ public BasicFetch generateFetch( final TableGroup tableGroup = sqlAstCreationState.getFromClauseAccess().getTableGroup( fetchParent.getNavigablePath() ); - // Since the expression is lazy, based on the available table reference joins, - // we need to force the initialization in case this is a fetch - tableDiscriminatorDetailsMap.forEach( - (tableName, tableDiscriminatorDetails) -> tableGroup.getTableReference( - fetchablePath, - tableName, - true - ) - ); + resolveSubTypeTableReferences( tableGroup, fetchablePath ); return super.generateFetch( fetchParent, fetchablePath, fetchTiming, selected, resultVariable, creationState ); } + private void resolveSubTypeTableReferences(TableGroup tableGroup, NavigablePath navigablePath) { + final EntityMappingType entityDescriptor = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); + // Since the expression is lazy, based on the available table reference joins, + // we need to force the initialization in case this is selected + for ( EntityMappingType subMappingType : entityDescriptor.getSubMappingTypes() ) { + tableGroup.getTableReference( + navigablePath, + subMappingType.getMappedTableDetails().getTableName(), + true + ); + } + } + @Override public Expression resolveSqlExpression( NavigablePath navigablePath, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index b1aea66004f3..53e1a0085708 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -205,6 +205,10 @@ public void finishInitialization(RuntimeModelCreationContext context) { registerEntityNameResolvers( persister, entityNameResolvers ); } + for ( EntityPersister persister : entityPersisterMap.values() ) { + persister.prepareLoaders(); + } + collectionPersisterMap.values().forEach( CollectionPersister::postInstantiate ); registerEmbeddableMappingType( bootModel ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 057d4d31ab08..eb9f849b31b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -28,6 +28,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; @@ -127,7 +128,6 @@ import org.hibernate.jdbc.TooManyRowsAffectedException; import org.hibernate.loader.ast.internal.MultiKeyLoadHelper; import org.hibernate.loader.ast.internal.CacheEntityLoaderHelper; -import org.hibernate.engine.profile.internal.FetchProfileAffectee; import org.hibernate.loader.ast.internal.LoaderSelectBuilder; import org.hibernate.loader.ast.internal.LoaderSqlAstCreationState; import org.hibernate.loader.ast.internal.MultiIdEntityLoaderArrayParam; @@ -205,6 +205,7 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.EntityInstantiator; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; @@ -232,6 +233,7 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.Alias; import org.hibernate.sql.Delete; +import org.hibernate.sql.InFragment; import org.hibernate.sql.SimpleSelect; import org.hibernate.sql.Template; import org.hibernate.sql.ast.spi.SimpleFromClauseAccessImpl; @@ -307,6 +309,7 @@ import static org.hibernate.generator.EventType.UPDATE; import static org.hibernate.internal.util.ReflectHelper.isAbstractClass; import static org.hibernate.internal.util.StringHelper.isEmpty; +import static org.hibernate.internal.util.StringHelper.qualifyConditionally; import static org.hibernate.internal.util.collections.ArrayHelper.contains; import static org.hibernate.internal.util.collections.ArrayHelper.to2DStringArray; import static org.hibernate.internal.util.collections.ArrayHelper.toBooleanArray; @@ -3171,6 +3174,68 @@ else if ( discriminatorValue == NOT_NULL_DISCRIMINATOR ) { return predicate; } + protected String getPrunedDiscriminatorPredicate( + Map entityNameUses, + MappingMetamodelImplementor mappingMetamodel, + String alias) { + final InFragment frag = new InFragment(); + if ( isDiscriminatorFormula() ) { + frag.setFormula( alias, getDiscriminatorFormulaTemplate() ); + } + else { + frag.setColumn( alias, getDiscriminatorColumnName() ); + } + boolean containsNotNull = false; + for ( Map.Entry entry : entityNameUses.entrySet() ) { + final EntityNameUse.UseKind useKind = entry.getValue().getKind(); + if ( useKind == EntityNameUse.UseKind.PROJECTION || useKind == EntityNameUse.UseKind.EXPRESSION ) { + // We only care about treat and filter uses which allow to reduce the amount of rows to select + continue; + } + final EntityPersister persister = mappingMetamodel.getEntityDescriptor( entry.getKey() ); + // Filtering for abstract entities makes no sense, so ignore that + // Also, it makes no sense to filter for any of the super types, + // as the query will contain a filter for that already anyway + if ( !persister.isAbstract() && ( this == persister || !isTypeOrSuperType( persister ) ) ) { + containsNotNull = containsNotNull || InFragment.NOT_NULL.equals( persister.getDiscriminatorSQLValue() ); + frag.addValue( persister.getDiscriminatorSQLValue() ); + } + } + final List discriminatorSQLValues = Arrays.asList( ( (AbstractEntityPersister) getRootEntityDescriptor() ).fullDiscriminatorSQLValues ); + if ( frag.getValues().size() == discriminatorSQLValues.size() ) { + // Nothing to prune if we filter for all subtypes + return null; + } + + if ( containsNotNull ) { + final String lhs; + if ( isDiscriminatorFormula() ) { + lhs = StringHelper.replace( getDiscriminatorFormulaTemplate(), Template.TEMPLATE, alias ); + } + else { + lhs = qualifyConditionally( alias, getDiscriminatorColumnName() ); + } + final List actualDiscriminatorSQLValues = new ArrayList<>( discriminatorSQLValues.size() ); + for ( String value : discriminatorSQLValues ) { + if ( !frag.getValues().contains( value ) && !InFragment.NULL.equals( value ) ) { + actualDiscriminatorSQLValues.add( value ); + } + } + final StringBuilder sb = new StringBuilder( 70 + actualDiscriminatorSQLValues.size() * 10 ).append( " or " ); + if ( !actualDiscriminatorSQLValues.isEmpty() ) { + sb.append( lhs ).append( " is not in (" ); + sb.append( String.join( ",", actualDiscriminatorSQLValues ) ); + sb.append( ") and " ); + } + sb.append( lhs ).append( " is not null" ); + frag.getValues().remove( InFragment.NOT_NULL ); + return frag.toFragmentString() + sb; + } + else { + return frag.toFragmentString(); + } + } + @Override public void applyFilterRestrictions( Consumer predicateConsumer, @@ -3526,6 +3591,10 @@ protected String substituteBrackets(String sql) { @Override public final void postInstantiate() throws MappingException { doLateInit(); + } + + @Override + public void prepareLoaders() { prepareLoader( singleIdLoader ); prepareLoader( multiIdLoader ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index a45e013ae4b4..2160929b2963 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -119,6 +119,17 @@ public interface EntityPersister extends EntityMappingType, RootTableGroupProduc */ void postInstantiate() throws MappingException; + /** + * Prepare loaders associated with the persister. Distinct "phase" + * in building the persister after {@linkplain InFlightEntityMappingType#prepareMappingModel} + * and {@linkplain #postInstantiate()} have occurred. + *

+ * The distinct phase is used to ensure that all {@linkplain org.hibernate.metamodel.mapping.TableDetails} + * are available across the entire model + */ + default void prepareLoaders() { + } + /** * Return the {@link org.hibernate.SessionFactory} to which this persister * belongs. diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index dba24f4be554..0a71cf92ed59 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -51,6 +51,7 @@ import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.spi.NavigablePath; @@ -75,6 +76,7 @@ import org.jboss.logging.Logger; import static java.util.Collections.emptyMap; +import static org.hibernate.internal.util.collections.ArrayHelper.indexOf; import static org.hibernate.internal.util.collections.ArrayHelper.to2DStringArray; import static org.hibernate.internal.util.collections.ArrayHelper.toIntArray; import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray; @@ -1271,7 +1273,8 @@ public FilterAliasGenerator getFilterAliasGenerator(String rootAlias) { @Override public TableDetails getMappedTableDetails() { - return getTableMapping( getTableMappings().length - 1 ); + // Subtract the number of secondary tables (tableSpan - coreTableSpan) and get the last table mapping + return getTableMapping( getTableMappings().length - ( tableSpan - coreTableSpan ) - 1 ); } @Override @@ -1314,6 +1317,7 @@ public void pruneForSubclasses(TableGroup tableGroup, Map // i.e. with parenthesis around, as that means the table reference joins will be isolated final boolean innerJoinOptimization = tableGroup.canUseInnerJoins() || tableGroup.isRealTableGroup(); final Set tablesToInnerJoin = innerJoinOptimization ? new HashSet<>() : null; + boolean needsTreatDiscriminator = false; for ( Map.Entry entry : entityNameUses.entrySet() ) { final EntityNameUse.UseKind useKind = entry.getValue().getKind(); final JoinedSubclassEntityPersister persister = @@ -1355,15 +1359,22 @@ public void pruneForSubclasses(TableGroup tableGroup, Map } } } + final String tableName = persister.getTableName(); final TableReference mainTableReference = tableGroup.getTableReference( null, - persister.getTableName(), + tableName, false ); - if ( mainTableReference == null ) { - throw new UnknownTableReferenceException( persister.getTableName(), "Couldn't find table reference" ); + if ( mainTableReference != null ) { + retainedTableReferences.add( mainTableReference ); + } + if ( needsDiscriminator() ) { + // We allow multiple joined subclasses to use the same table if they define a discriminator column. + // In this case, we might need to add a discriminator condition to make sure we filter the correct subtype, + // see SingleTableEntityPersister#pruneForSubclasses for more details on this condition + needsTreatDiscriminator = needsTreatDiscriminator || !persister.isAbstract() && + !isTypeOrSuperType( persister ) && useKind == EntityNameUse.UseKind.TREAT; } - retainedTableReferences.add( mainTableReference ); } // If no tables to inner join have been found, we add at least the super class tables of this persister if ( innerJoinOptimization && tablesToInnerJoin.isEmpty() ) { @@ -1376,6 +1387,30 @@ public void pruneForSubclasses(TableGroup tableGroup, Map } final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + if ( needsTreatDiscriminator ) { + if ( tableReferenceJoins.isEmpty() ) { + // We need to apply the discriminator predicate to the primary table reference itself + final String discriminatorPredicate = getPrunedDiscriminatorPredicate( entityNameUses, metamodel, "t" ); + if ( discriminatorPredicate != null ) { + final NamedTableReference tableReference = (NamedTableReference) tableGroup.getPrimaryTableReference(); + tableReference.setPrunedTableExpression( "(select * from " + getRootTableName() + " t where " + discriminatorPredicate + ")" ); + } + } + else { + // We have to apply the discriminator condition to the root table reference join + boolean applied = applyDiscriminatorPredicate( + tableReferenceJoins.get( 0 ), + (NamedTableReference) tableGroup.getPrimaryTableReference(), + entityNameUses, + metamodel + ); + for ( int i = 0; !applied && i < tableReferenceJoins.size(); i++ ) { + final TableReferenceJoin join = tableReferenceJoins.get( i ); + applied = applyDiscriminatorPredicate( join, join.getJoinedTableReference(), entityNameUses, metamodel ); + } + assert applied : "Could not apply treat discriminator predicate to root table join"; + } + } if ( tableReferenceJoins.isEmpty() ) { return; } @@ -1394,9 +1429,8 @@ public void pruneForSubclasses(TableGroup tableGroup, Map tableReferenceJoins.add( join ); } else { - final String tableExpression = oldJoin.getJoinedTableReference().getTableExpression(); for ( int i = subclassCoreTableSpan; i < subclassTableNameClosure.length; i++ ) { - if ( tableExpression.equals( subclassTableNameClosure[i] ) ) { + if ( joinedTableReference.getTableExpression().equals( subclassTableNameClosure[i] ) ) { // Retain joins to secondary tables tableReferenceJoins.add( oldJoin ); break; @@ -1410,6 +1444,24 @@ public void pruneForSubclasses(TableGroup tableGroup, Map } } + private boolean applyDiscriminatorPredicate( + TableReferenceJoin join, + NamedTableReference tableReference, + Map entityNameUses, + MappingMetamodelImplementor metamodel) { + if ( tableReference.getTableExpression().equals( getRootTableName() ) ) { + assert join.getJoinType() == SqlAstJoinType.INNER : "Found table reference join with root table of non-INNER type: " + join.getJoinType(); + final String discriminatorPredicate = getPrunedDiscriminatorPredicate( + entityNameUses, + metamodel, + tableReference.getIdentificationVariable() + ); + join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); + return true; + } + return false; + } + @Override public void visitConstraintOrderedTables(ConstraintOrderedTableConsumer consumer) { for ( int i = 0; i < constraintOrderedTableNames.length; i++ ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java index 7a1afe84fd22..c2898a488d38 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java @@ -8,7 +8,6 @@ import java.io.Serializable; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,7 +22,6 @@ import org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle; import org.hibernate.internal.DynamicFilterAliasGenerator; import org.hibernate.internal.FilterAliasGenerator; -import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.jdbc.Expectation; import org.hibernate.mapping.Column; @@ -40,8 +38,6 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.query.sqm.function.SqmFunctionRegistry; -import org.hibernate.sql.InFragment; -import org.hibernate.sql.Template; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; @@ -693,66 +689,10 @@ public void pruneForSubclasses(TableGroup tableGroup, Map return; } - final InFragment frag = new InFragment(); - if ( isDiscriminatorFormula() ) { - frag.setFormula( "t", getDiscriminatorFormulaTemplate() ); - } - else { - frag.setColumn( "t", getDiscriminatorColumnName() ); - } - boolean containsNotNull = false; - for ( Map.Entry entry : entityNameUses.entrySet() ) { - final EntityNameUse.UseKind useKind = entry.getValue().getKind(); - if ( useKind == EntityNameUse.UseKind.PROJECTION || useKind == EntityNameUse.UseKind.EXPRESSION ) { - // We only care about treat and filter uses which allow to reduce the amount of rows to select - continue; - } - final EntityPersister persister = mappingMetamodel.getEntityDescriptor( entry.getKey() ); - // Filtering for abstract entities makes no sense, so ignore that - // Also, it makes no sense to filter for any of the super types, - // as the query will contain a filter for that already anyway - if ( !persister.isAbstract() && ( this == persister || !isTypeOrSuperType( persister ) ) ) { - containsNotNull = containsNotNull || InFragment.NOT_NULL.equals( persister.getDiscriminatorSQLValue() ); - frag.addValue( persister.getDiscriminatorSQLValue() ); - } - } - final List discriminatorSQLValues = Arrays.asList( ( (SingleTableEntityPersister) getRootEntityDescriptor() ).fullDiscriminatorSQLValues ); - if ( frag.getValues().size() == discriminatorSQLValues.size() ) { - // Nothing to prune if we filter for all subtypes - return; - } - - final NamedTableReference tableReference = (NamedTableReference) tableGroup.getPrimaryTableReference(); - if ( containsNotNull ) { - final String lhs; - if ( isDiscriminatorFormula() ) { - lhs = StringHelper.replace( getDiscriminatorFormulaTemplate(), Template.TEMPLATE, "t" ); - } - else { - lhs = "t." + getDiscriminatorColumnName(); - } - final List actualDiscriminatorSQLValues = new ArrayList<>( discriminatorSQLValues.size() ); - for ( String value : discriminatorSQLValues ) { - if ( !frag.getValues().contains( value ) && !InFragment.NULL.equals( value ) ) { - actualDiscriminatorSQLValues.add( value ); - } - } - final StringBuilder sb = new StringBuilder( 70 + actualDiscriminatorSQLValues.size() * 10 ).append( " or " ); - if ( !actualDiscriminatorSQLValues.isEmpty() ) { - sb.append( lhs ).append( " is not in (" ); - sb.append( String.join( ",", actualDiscriminatorSQLValues ) ); - sb.append( ") and " ); - } - sb.append( lhs ).append( " is not null" ); - frag.getValues().remove( InFragment.NOT_NULL ); - tableReference.setPrunedTableExpression( - "(select * from " + getTableName() + " t where " + frag.toFragmentString() + sb + ")" - ); - } - else { - tableReference.setPrunedTableExpression( - "(select * from " + getTableName() + " t where " + frag.toFragmentString() + ")" - ); + final String discriminatorPredicate = getPrunedDiscriminatorPredicate( entityNameUses, mappingMetamodel, "t" ); + if ( discriminatorPredicate != null ) { + final NamedTableReference tableReference = (NamedTableReference) tableGroup.getPrimaryTableReference(); + tableReference.setPrunedTableExpression( "(select * from " + getTableName() + " t where " + discriminatorPredicate + ")" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index a29f5f6537df..3f6785a2ac34 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -18,17 +18,20 @@ import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; -import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.jboss.logging.Logger; +import static org.hibernate.query.sqm.internal.SqmUtil.findCompatibleFetchJoin; + /** * Specialized "intermediate" SemanticPathPart for processing domain model paths. * @@ -43,6 +46,7 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer { private final SqmJoinType joinType; private final boolean fetch; private final String alias; + private final boolean allowReuse; private ConsumerDelegate delegate; private boolean nested; @@ -52,11 +56,13 @@ public QualifiedJoinPathConsumer( SqmJoinType joinType, boolean fetch, String alias, + boolean allowReuse, SqmCreationState creationState) { this.sqmRoot = sqmRoot; this.joinType = joinType; this.fetch = fetch; this.alias = alias; + this.allowReuse = allowReuse; this.creationState = creationState; } @@ -70,6 +76,8 @@ public QualifiedJoinPathConsumer( this.joinType = joinType; this.fetch = fetch; this.alias = alias; + // This constructor is only used for entity names, so no need for join reuse + this.allowReuse = false; this.creationState = creationState; this.delegate = new AttributeJoinDelegate( sqmFrom, @@ -100,7 +108,13 @@ public void consumeIdentifier(String identifier, boolean isBase, boolean isTermi } else { assert delegate != null; - delegate.consumeIdentifier( identifier, !nested && isTerminal, !( nested && isTerminal ) ); + delegate.consumeIdentifier( + identifier, + !nested && isTerminal, + // Non-nested joins shall allow reuse, but nested ones (i.e. in treat) + // only allow join reuse for non-terminal parts + allowReuse && (!nested || !isTerminal) + ); } } @@ -179,11 +193,28 @@ private static SqmFrom createJoin( boolean allowReuse, SqmCreationState creationState) { final SqmPathSource subPathSource = lhs.getResolvedModel().getSubPathSource( name ); - if ( allowReuse && !isTerminal ) { - for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { - if ( sqmJoin.getAlias() == null && sqmJoin.getReferencedPathSource() == subPathSource ) { + if ( allowReuse ) { + if ( !isTerminal ) { + for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { + // In order for an HQL join to be reusable, is must have the same path source, + if ( sqmJoin.getModel() == subPathSource + // must not have a join condition + && ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null + // and the same join type + && sqmJoin.getSqmJoinType() == joinType ) { + //noinspection unchecked + return (SqmFrom) sqmJoin; + } + } + } + else if ( fetch ) { + final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( lhs, subPathSource, joinType ); + if ( compatibleFetchJoin != null ) { + if ( alias != null ) { + throw new IllegalStateException( "Cannot fetch the same association twice with a different alias" ); + } //noinspection unchecked - return (SqmFrom) sqmJoin; + return (SqmFrom) compatibleFetchJoin; } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index bcd72a287d27..1a534c312e99 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2180,44 +2180,21 @@ protected void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmR throw new SemanticException( "fetch not allowed in subquery from-elements" ); } + final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction(); + // Joins are allowed to be reused if they don't have a join condition + final boolean allowReuse = joinRestrictionContext == null; dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, + allowReuse, this ) ); try { - final SqmQualifiedJoin join; - if ( qualifiedJoinTargetContext instanceof HqlParser.JoinPathContext ) { - //noinspection unchecked - join = (SqmQualifiedJoin) qualifiedJoinTargetContext.getChild( 0 ).accept( this ); - } - else { - if ( fetch ) { - throw new SemanticException( "fetch not allowed for subquery join" ); - } - if ( getCreationOptions().useStrictJpaCompliance() ) { - throw new StrictJpaComplianceViolation( - "The JPA specification does not support subqueries in the from clause. " + - "Please disable the JPA query compliance if you want to use this feature.", - StrictJpaComplianceViolation.Type.FROM_SUBQUERY - ); - } - final TerminalNode terminalNode = (TerminalNode) qualifiedJoinTargetContext.getChild( 0 ); - final boolean lateral = terminalNode.getSymbol().getType() == HqlParser.LATERAL; - final int subqueryIndex = lateral ? 2 : 1; - final DotIdentifierConsumer identifierConsumer = dotIdentifierConsumerStack.pop(); - final SqmSubQuery subQuery = (SqmSubQuery) qualifiedJoinTargetContext.getChild( subqueryIndex ).accept( this ); - dotIdentifierConsumerStack.push( identifierConsumer ); - //noinspection unchecked,rawtypes - join = new SqmDerivedJoin( subQuery, alias, joinType, lateral, sqmRoot ); - processingStateStack.getCurrent().getPathRegistry().register( join ); - } - - final HqlParser.JoinRestrictionContext qualifiedJoinRestrictionContext = parserJoin.joinRestriction(); + final SqmQualifiedJoin join = getJoin( sqmRoot, joinType, qualifiedJoinTargetContext, alias, fetch ); if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin || join instanceof SqmCteJoin ) { sqmRoot.addSqmJoin( join ); } @@ -2233,15 +2210,15 @@ else if ( join instanceof SqmAttributeJoin ) { } } } - if ( qualifiedJoinRestrictionContext != null && attributeJoin.isFetched() ) { + if ( joinRestrictionContext != null && attributeJoin.isFetched() ) { throw new SemanticException( "with-clause not allowed on fetched associations; use filters" ); } } - if ( qualifiedJoinRestrictionContext != null ) { + if ( joinRestrictionContext != null ) { dotIdentifierConsumerStack.push( new QualifiedJoinPredicatePathConsumer( join, this ) ); try { - join.setJoinPredicate( (SqmPredicate) qualifiedJoinRestrictionContext.getChild( 1 ).accept( this ) ); + join.setJoinPredicate( (SqmPredicate) joinRestrictionContext.getChild( 1 ).accept( this ) ); } finally { dotIdentifierConsumerStack.pop(); @@ -2253,6 +2230,43 @@ else if ( join instanceof SqmAttributeJoin ) { } } + @SuppressWarnings("unchecked") + private SqmQualifiedJoin getJoin( + SqmRoot sqmRoot, + SqmJoinType joinType, + HqlParser.JoinTargetContext joinTargetContext, + String alias, + boolean fetch) { + if ( joinTargetContext instanceof HqlParser.JoinPathContext ) { + final HqlParser.JoinPathContext joinPathContext = (HqlParser.JoinPathContext) joinTargetContext; + return (SqmQualifiedJoin) joinPathContext.path().accept( this ); + } + else if ( joinTargetContext instanceof HqlParser.JoinSubqueryContext ) { + if ( fetch ) { + throw new SemanticException( "The 'from' clause of a subquery has a 'fetch' join" ); + } + if ( getCreationOptions().useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "The JPA specification does not support subqueries in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature.", + StrictJpaComplianceViolation.Type.FROM_SUBQUERY + ); + } + + final HqlParser.JoinSubqueryContext joinSubqueryContext = (HqlParser.JoinSubqueryContext) joinTargetContext; + final boolean lateral = joinSubqueryContext.LATERAL() != null; + final DotIdentifierConsumer identifierConsumer = dotIdentifierConsumerStack.pop(); + final SqmSubQuery subQuery = (SqmSubQuery) joinSubqueryContext.subquery().accept( this ); + dotIdentifierConsumerStack.push( identifierConsumer ); + final SqmQualifiedJoin join = new SqmDerivedJoin<>( subQuery, alias, joinType, lateral, sqmRoot ); + processingStateStack.getCurrent().getPathRegistry().register( join ); + return join; + } + else { + throw new ParsingException( "unexpected join type" ); + } + } + @Override public SqmJoin visitJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx) { throw new UnsupportedOperationException(); @@ -2276,7 +2290,7 @@ protected void consumeJpaCollectionJoin( SqmJoinType.INNER, false, alias, - this + true,this ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index e6f353c8b819..fbefe443b0a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -39,6 +39,8 @@ import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterImplementor; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.spi.JdbcParameterBySqmParameterAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; @@ -51,6 +53,7 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; @@ -231,6 +234,32 @@ public static List getOrderByNavigablePaths(SqmQuerySpec query return navigablePaths; } + public static SqmAttributeJoin findCompatibleFetchJoin( + SqmFrom sqmFrom, + SqmPathSource pathSource, + SqmJoinType requestedJoinType) { + for ( final SqmJoin join : sqmFrom.getSqmJoins() ) { + if ( join.getModel() == pathSource ) { + final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) join; + if ( attributeJoin.isFetched() ) { + final SqmJoinType joinType = join.getSqmJoinType(); + if ( joinType != requestedJoinType ) { + throw new IllegalStateException( String.format( + "Requested join fetch with association [%s] with '%s' join type, " + + "but found existing join fetch with '%s' join type.", + pathSource.getPathName(), + requestedJoinType, + joinType + ) ); + } + //noinspection unchecked + return (SqmAttributeJoin) attributeJoin; + } + } + } + return null; + } + public static Map, Map, List>> generateJdbcParamsXref( DomainParameterXref domainParameterXref, JdbcParameterBySqmParameterAccess jdbcParameterBySqmParameterAccess) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java index 1c43c1ae9b34..858ae81d2fce 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java @@ -8,11 +8,19 @@ import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmJunctionPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.spi.NavigablePath; import org.hibernate.query.sqm.tree.domain.SqmPath; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; +import jakarta.persistence.criteria.Predicate; + +import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; + /** * @author Steve Ebersole */ @@ -68,6 +76,87 @@ public static NavigablePath buildSubNavigablePath(SqmPath lhs, String subNavi return buildSubNavigablePath( navigablePath, subNavigable, alias ); } + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, List incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.size(); i++ ) { + combined = combinePredicates( combined, (SqmPredicate) incomingRestrictions.get(i) ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, JpaPredicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, Predicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, SqmPredicate incomingRestriction) { + if ( baseRestriction == null ) { + return incomingRestriction; + } + + if ( incomingRestriction == null ) { + return baseRestriction; + } + + final SqmJunctionPredicate combinedPredicate; + + if ( baseRestriction instanceof SqmJunctionPredicate ) { + final SqmJunctionPredicate junction = (SqmJunctionPredicate) baseRestriction; + // we already had multiple before + if ( junction.getPredicates().isEmpty() ) { + return incomingRestriction; + } + + if ( junction.getOperator() == Predicate.BooleanOperator.AND ) { + combinedPredicate = junction; + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + + combinedPredicate.getPredicates().add( incomingRestriction ); + + return combinedPredicate; + } + private SqmCreationHelper() { } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index c0e75d11fe9c..e0ceb7bf3b5d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -32,7 +32,6 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.LockMode; -import org.hibernate.QueryException; import org.hibernate.boot.model.process.internal.InferredBasicValueResolver; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.Dialect; @@ -50,7 +49,7 @@ import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.enhanced.Optimizer; import org.hibernate.internal.FilterHelper; -import org.hibernate.internal.util.MutableObject; +import org.hibernate.internal.util.MutableBoolean; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; @@ -147,6 +146,7 @@ import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.produce.function.internal.PatternRenderer; import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.sql.internal.AnyDiscriminatorPathInterpretation; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; import org.hibernate.query.sqm.sql.internal.DiscriminatedAssociationPathInterpretation; @@ -285,6 +285,7 @@ import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.SqlTreeCreationLogger; +import org.hibernate.sql.ast.internal.TableGroupJoinHelper; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; @@ -2666,7 +2667,12 @@ protected void consumeFromClauseCorrelatedRoot(SqmRoot sqmRoot) { // as roots anyway, so nothing to worry about log.tracef( "Resolved SqmRoot [%s] to correlated TableGroup [%s]", sqmRoot, tableGroup ); - consumeExplicitJoins( from, tableGroup ); + if ( from instanceof SqmRoot ) { + consumeJoins( (SqmRoot) from, fromClauseIndex, tableGroup ); + } + else { + consumeExplicitJoins( from, tableGroup ); + } return; } else { @@ -3080,12 +3086,14 @@ private void registerEntityNameUsage( (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); - // Resolve the table reference for all types which we register an entity name use for - if ( actualTableGroup.isInitialized() ) { + // Resolve the table reference for all types which we register an entity name use for. + // Also, force table group initialization for treats when needed to ensure correct cardinality + final EntityNameUse.UseKind useKind = finalEntityNameUse.getKind(); + if ( actualTableGroup.isInitialized() || ( useKind == EntityNameUse.UseKind.TREAT && actualTableGroup.canUseInnerJoins() + && !( (EntityMappingType) actualTableGroup.getModelPart().getPartMappingType() ).isTypeOrSuperType( persister ) ) ) { actualTableGroup.resolveTableReference( null, persister.getTableName() ); } - final EntityNameUse.UseKind useKind = finalEntityNameUse.getKind(); if ( projection ) { EntityMappingType superMappingType = persister; while ( ( superMappingType = superMappingType.getSuperMappingType() ) != null ) { @@ -3096,17 +3104,22 @@ private void registerEntityNameUsage( ); } } - if ( useKind == EntityNameUse.UseKind.TREAT || useKind == EntityNameUse.UseKind.PROJECTION ) { - // If we encounter a treat use, we also want register the use for all subtypes. - // We do this here to not have to expand entity name uses during pruning later on + + // If we encounter a treat or projection use, we also want register the use for all subtypes. + // We do this here to not have to expand entity name uses during pruning later on + if ( useKind == EntityNameUse.UseKind.TREAT ) { for ( EntityMappingType subType : persister.getSubMappingTypes() ) { entityNameUses.compute( subType.getEntityName(), (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); - actualTableGroup.resolveTableReference( - null, - subType.getEntityPersister().getMappedTableDetails().getTableName() + } + } + else if ( useKind == EntityNameUse.UseKind.PROJECTION ) { + for ( EntityMappingType subType : persister.getSubMappingTypes() ) { + entityNameUses.compute( + subType.getEntityName(), + (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); } } @@ -3271,6 +3284,39 @@ private TableGroup consumeAttributeJoin( SqmMappingModelHelper.resolveExplicitTreatTarget( sqmJoin, this ) ); + final List> sqmTreats = sqmJoin.getSqmTreats(); + final SqmPredicate joinPredicate; + final SqmPredicate[] treatPredicates; + final boolean hasPredicate; + if ( !sqmTreats.isEmpty() ) { + if ( sqmTreats.size() == 1 ) { + // If there is only a single treat, combine the predicates just as they are + joinPredicate = SqmCreationHelper.combinePredicates( + sqmJoin.getJoinPredicate(), + ( (SqmQualifiedJoin) sqmTreats.get( 0 ) ).getJoinPredicate() + ); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + else { + // When there are multiple predicates, we have to apply type filters + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = new SqmPredicate[sqmTreats.size()]; + boolean hasTreatPredicate = false; + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var p = ( (SqmQualifiedJoin) sqmTreats.get( i ) ).getJoinPredicate(); + treatPredicates[i] = p; + hasTreatPredicate = hasTreatPredicate || p != null; + } + hasPredicate = joinPredicate != null || hasTreatPredicate; + } + } + else { + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + if ( pathSource instanceof PluralPersistentAttribute ) { assert modelPart instanceof PluralAttributeMapping; @@ -3287,7 +3333,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3303,7 +3349,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3312,7 +3358,7 @@ private TableGroup consumeAttributeJoin( // Since this is an explicit join, we force the initialization of a possible lazy table group // to retain the cardinality, but only if this is a non-trivial attribute join. // Left or inner singular attribute joins without a predicate can be safely optimized away - if ( sqmJoin.getJoinPredicate() != null || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { + if ( hasPredicate || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { joinedTableGroup.getPrimaryTableReference(); } } @@ -3326,16 +3372,58 @@ private TableGroup consumeAttributeJoin( registerEntityNameProjectionUsage( sqmJoin, getActualTableGroup( joinedTableGroup, sqmJoin ) ); } registerPathAttributeEntityNameUsage( sqmJoin, ownerTableGroup ); + if ( !sqmJoin.hasTreats() && sqmJoin.getNodeType().getSqmPathType() instanceof EntityDomainType ) { + final EntityDomainType entityDomainType = (EntityDomainType) sqmJoin.getNodeType().getSqmPathType(); + final TableGroup elementTableGroup = joinedTableGroup instanceof PluralTableGroup ? + ( (PluralTableGroup) joinedTableGroup ).getElementTableGroup() : + joinedTableGroup; + final EntityValuedModelPart entityModelPart = (EntityValuedModelPart) elementTableGroup.getModelPart(); + final EntityPersister entityDescriptor = entityModelPart.getEntityMappingType().getEntityPersister(); + if ( entityDescriptor.getSuperMappingType() != null ) { + // This is a non-treated join with an entity which is an inheritance subtype, + // register a TREAT entity name use to filter only the entities of the correct type. + registerEntityNameUsage( + getActualTableGroup( joinedTableGroup, sqmJoin ), + EntityNameUse.TREAT, + entityDomainType.getHibernateEntityName() + ); + } + } + + // Implicit joins in the predicate might alter the nested table group joins, + // so defer determination of the join for predicate until after the predicate was visited + final TableGroupJoin joinForPredicate; // add any additional join restrictions - if ( sqmJoin.getJoinPredicate() != null ) { + if ( hasPredicate ) { if ( sqmJoin.isFetched() ) { QueryLogging.QUERY_MESSAGE_LOGGER.debugf( "Join fetch [%s] is restricted", sqmJoinNavigablePath ); } final SqmJoin oldJoin = currentlyProcessingJoin; currentlyProcessingJoin = sqmJoin; - joinedTableGroupJoin.applyPredicate( visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ) ); + Predicate predicate = joinPredicate == null ? null : visitNestedTopLevelPredicate( joinPredicate ); + if ( treatPredicates != null ) { + final Junction orPredicate = new Junction( Junction.Nature.DISJUNCTION ); + for ( int i = 0; i < treatPredicates.length; i++ ) { + final EntityDomainType treatType = + (EntityDomainType) ( (SqmTreatedPath) sqmTreats.get( i ) ).getTreatTarget(); + orPredicate.add( combinePredicates( + createTreatTypeRestriction( sqmJoin, treatType ), + treatPredicates[i] == null ? null : visitNestedTopLevelPredicate( treatPredicates[i] ) + ) ); + } + predicate = predicate != null ? combinePredicates( predicate, orPredicate ) : orPredicate; + } + joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin ); + // If translating the join predicate didn't initialize the table group, + // we can safely apply it on the collection table group instead + if ( joinForPredicate.getJoinedGroup().isInitialized() ) { + joinForPredicate.applyPredicate( predicate ); + } + else { + joinedTableGroupJoin.applyPredicate( predicate ); + } currentlyProcessingJoin = oldJoin; } // Since joins on treated paths will never cause table pruning, we need to add a join condition for the treat @@ -3384,7 +3472,7 @@ private TableGroup consumeCrossJoin(SqmCrossJoin sqmJoin, TableGroup lhsTable } private TableGroup consumeEntityJoin(SqmEntityJoin sqmJoin, TableGroup lhsTableGroup, boolean transitive) { - final MutableObject predicate = new MutableObject<>(); + final MutableBoolean needsTreat = new MutableBoolean( false ); final EntityPersister entityDescriptor = resolveEntityPersister( sqmJoin.getReferencedPathSource() ); final SqlAstJoinType correspondingSqlJoinType = sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(); @@ -3393,16 +3481,21 @@ private TableGroup consumeEntityJoin(SqmEntityJoin sqmJoin, TableGroup lhsTab sqmJoin.getNavigablePath(), sqmJoin.getExplicitAlias(), null, - () -> p -> predicate.set( combinePredicates( predicate.get(), p ) ), + () -> p -> needsTreat.setValue( true ), this ); registerSqmFromTableGroup( sqmJoin, tableGroup ); + if ( needsTreat.getValue() ) { + // Register new treat to apply the discriminator condition to the table reference itself, see #pruneTableGroupJoins + registerEntityNameUsage( tableGroup, EntityNameUse.TREAT, entityDescriptor.getEntityName() ); + } + final TableGroupJoin tableGroupJoin = new TableGroupJoin( sqmJoin.getNavigablePath(), correspondingSqlJoinType, tableGroup, - predicate.get() + null ); lhsTableGroup.addTableGroupJoin( tableGroupJoin ); @@ -4555,6 +4648,7 @@ public Object visitMapEntryFunction(SqmMapEntryReference entryRef) { null, this ); + registerProjectionUsageFromDescriptor( tableGroup, indexDescriptor ); final CollectionPart valueDescriptor = mapDescriptor.getElementDescriptor(); final NavigablePath valueNavigablePath = mapNavigablePath.append( valueDescriptor.getPartName() ); @@ -4564,6 +4658,7 @@ public Object visitMapEntryFunction(SqmMapEntryReference entryRef) { null, this ); + registerProjectionUsageFromDescriptor( tableGroup, valueDescriptor ); return new DomainResultProducer>() { @Override @@ -4583,6 +4678,14 @@ public void applySqlSelections(DomainResultCreationState creationState) { }; } + private void registerProjectionUsageFromDescriptor(TableGroup tableGroup, CollectionPart descriptor) { + if ( descriptor instanceof EntityCollectionPart ) { + final EntityCollectionPart entityCollectionPart = (EntityCollectionPart) descriptor; + final EntityMappingType entityMappingType = entityCollectionPart.getEntityMappingType(); + registerEntityNameUsage( tableGroup, EntityNameUse.PROJECTION, entityMappingType.getEntityName(), true ); + } + } + protected Expression createCorrelatedAggregateSubQuery( AbstractSqmSpecificPluralPartPath pluralPartPath, boolean index, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 06869348a4cf..9efcf73f1b32 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -61,6 +61,8 @@ import jakarta.persistence.metamodel.SetAttribute; import jakarta.persistence.metamodel.SingularAttribute; +import static org.hibernate.query.sqm.internal.SqmUtil.findCompatibleFetchJoin; + /** * Convenience base class for SqmFrom implementations * @@ -165,7 +167,7 @@ public SqmPath resolvePathPart( if ( sqmJoin instanceof SqmSingularJoin && name.equals( sqmJoin.getReferencedPathSource().getPathName() ) ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( attributeJoin.getOn() == null ) { + if ( attributeJoin.getJoinPredicate() == null ) { // todo (6.0): to match the expectation of the JPA spec I think we also have to check // that the join type is INNER or the default join type for the attribute, // but as far as I understand, in 5.x we expect to ignore this behavior @@ -648,8 +650,18 @@ public SqmSingularJoin fetch(SingularAttribute attribute) @Override @SuppressWarnings("unchecked") public SqmSingularJoin fetch(SingularAttribute attribute, JoinType jt) { + final SingularPersistentAttribute persistentAttribute = (SingularPersistentAttribute) attribute; + final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( + this, + persistentAttribute, + SqmJoinType.from( jt ) + ); + if ( compatibleFetchJoin != null ) { + return (SqmSingularJoin) compatibleFetchJoin; + } + final SqmSingularJoin join = buildSingularJoin( - (SingularPersistentAttribute) attribute, + persistentAttribute, SqmJoinType.from( jt ), true ); @@ -693,6 +705,11 @@ private SqmAttributeJoin buildJoin( SqmPathSource joinedPathSource, SqmJoinType joinType, boolean fetched) { + final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( this, joinedPathSource, joinType ); + if ( compatibleFetchJoin != null ) { + return compatibleFetchJoin; + } + final SqmAttributeJoin sqmJoin; if ( joinedPathSource instanceof SingularPersistentAttribute ) { sqmJoin = buildSingularJoin( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java index 195abf1d9e9e..049e646136f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java @@ -14,6 +14,8 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; /** * Contract representing a from clause. @@ -89,4 +91,158 @@ public int getNumberOfRoots() { return domainRoots.size(); } } + + public void appendHqlString(StringBuilder sb) { + String separator = " "; + for ( SqmRoot root : getRoots() ) { + sb.append( separator ); + if ( root.isCorrelated() ) { + if ( root.containsOnlyInnerJoins() ) { + appendJoins( root, root.getCorrelationParent().resolveAlias(), sb ); + } + else { + sb.append( root.getCorrelationParent().resolveAlias() ); + sb.append( ' ' ).append( root.resolveAlias() ); + appendJoins( root, sb ); + appendTreatJoins( root, sb ); + } + } + else { + sb.append( root.getEntityName() ); + sb.append( ' ' ).append( root.resolveAlias() ); + appendJoins( root, sb ); + appendTreatJoins( root, sb ); + } + separator = ", "; + } + } + + public static void appendJoins(SqmFrom sqmFrom, StringBuilder sb) { + if ( sqmFrom instanceof SqmRoot && ( (SqmRoot) sqmFrom ).getOrderedJoins() != null ) { + appendJoins( sqmFrom, ( (SqmRoot) sqmFrom ).getOrderedJoins(), sb, false ); + } + else { + appendJoins( sqmFrom, sqmFrom.getSqmJoins(), sb, true ); + } + } + + private static void appendJoins(SqmFrom sqmFrom, List> joins, StringBuilder sb, boolean transitive) { + for ( SqmJoin sqmJoin : joins ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); + if ( sqmJoin instanceof SqmAttributeJoin ) { + final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; + final List> sqmTreats = attributeJoin.getSqmTreats(); + if ( attributeJoin.getExplicitAlias() != null && !sqmTreats.isEmpty() ) { + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var treatJoin = (SqmAttributeJoin) sqmTreats.get( i ); + if ( i != 0 ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); + } + sb.append( "treat(" ); + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + sb.append( " as " ); + sb.append( ((SqmTreatedPath) treatJoin).getTreatTarget().getTypeName() ); + sb.append( ')' ); + appendJoinAliasAndOnClause( sb, treatJoin ); + if ( transitive ) { + appendJoins( treatJoin, sb ); + } + } + } + else { + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + appendJoinAliasAndOnClause( sb, attributeJoin ); + if ( transitive ) { + appendJoins( attributeJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } + } + } + else if ( sqmJoin instanceof SqmCrossJoin ) { + sb.append( ( (SqmCrossJoin) sqmJoin ).getEntityName() ); + sb.append( ' ' ).append( sqmJoin.resolveAlias() ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } + } + else if ( sqmJoin instanceof SqmEntityJoin ) { + final SqmEntityJoin sqmEntityJoin = (SqmEntityJoin) sqmJoin; + sb.append( sqmEntityJoin.getEntityName() ); + appendJoinAliasAndOnClause( sb, sqmEntityJoin ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } + } + else { + throw new UnsupportedOperationException( "Unsupported join: " + sqmJoin ); + } + } + } + + private static void appendJoinAliasAndOnClause(StringBuilder sb, SqmQualifiedJoin join) { + sb.append( ' ' ).append( join.resolveAlias() ); + if ( join.getJoinPredicate() != null ) { + sb.append( " on " ); + join.getJoinPredicate().appendHqlString( sb ); + } + } + + private static void appendAttributeJoin(SqmFrom sqmFrom, StringBuilder sb, SqmAttributeJoin attributeJoin) { + if ( sqmFrom instanceof SqmTreatedPath ) { + final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; + sb.append( "treat(" ); + treatedPath.getWrappedPath().appendHqlString( sb ); +// sb.append( treatedPath.getWrappedPath().resolveAlias( context ) ); + sb.append( " as " ).append( treatedPath.getTreatTarget().getTypeName() ).append( ')' ); + } + else { + sb.append( sqmFrom.resolveAlias() ); + } + sb.append( '.' ).append( attributeJoin.getAttribute().getName() ); + } + + private static void appendJoinType(StringBuilder sb, SqmJoinType sqmJoinType) { + final String joinText; + switch ( sqmJoinType ) { + case LEFT: + joinText = " left join "; + break; + case RIGHT: + joinText = " right join "; + break; + case INNER: + joinText = " join "; + break; + case FULL: + joinText = " full join "; + break; + case CROSS: + joinText = " cross join "; + break; + default: + throw new UnsupportedOperationException( "Unsupported join type: " + sqmJoinType ); + } + sb.append( joinText ); + } + + private void appendJoins(SqmFrom sqmFrom, String correlationPrefix, StringBuilder sb) { + String separator = ""; + for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { + assert sqmJoin instanceof SqmAttributeJoin; + sb.append( separator ); + sb.append( correlationPrefix ).append( '.' ); + sb.append( ( (SqmAttributeJoin) sqmJoin ).getAttribute().getName() ); + sb.append( ' ' ).append( sqmJoin.resolveAlias() ); + appendJoins( sqmJoin, sb ); + separator = ", "; + } + } + + public static void appendTreatJoins(SqmFrom sqmFrom, StringBuilder sb) { + for ( SqmFrom sqmTreat : sqmFrom.getSqmTreats() ) { + appendJoins( sqmTreat, sb ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java index 343f07a65c75..b36d4fcd2313 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java @@ -11,6 +11,7 @@ import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import jakarta.persistence.criteria.Expression; @@ -22,6 +23,15 @@ public class SqmJunctionPredicate extends AbstractSqmPredicate { private final BooleanOperator booleanOperator; private final List predicates; + public SqmJunctionPredicate( + BooleanOperator booleanOperator, + SqmExpressible expressible, + NodeBuilder nodeBuilder) { + super( expressible, nodeBuilder ); + this.booleanOperator = booleanOperator; + this.predicates = new ArrayList<>(); + } + public SqmJunctionPredicate( BooleanOperator booleanOperator, SqmPredicate leftHandPredicate, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java index 6ed60031bac1..29854b1b88ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java @@ -31,12 +31,9 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmNode; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; -import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; -import org.hibernate.query.sqm.tree.from.SqmCrossJoin; -import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmFromClauseContainer; @@ -581,29 +578,8 @@ public void appendHqlString(StringBuilder sb) { } } if ( fromClause != null ) { - sb.append( " from " ); - String separator = ""; - for ( SqmRoot root : fromClause.getRoots() ) { - sb.append( separator ); - if ( root.isCorrelated() ) { - if ( root.containsOnlyInnerJoins() ) { - appendJoins( root, root.getCorrelationParent().resolveAlias(), sb ); - } - else { - sb.append( root.getCorrelationParent().resolveAlias() ); - sb.append( ' ' ).append( root.resolveAlias() ); - appendJoins( root, sb ); - appendTreatJoins( root, sb ); - } - } - else { - sb.append( root.getEntityName() ); - sb.append( ' ' ).append( root.resolveAlias() ); - appendJoins( root, sb ); - appendTreatJoins( root, sb ); - } - separator = ", "; - } + sb.append( " from" ); + fromClause.appendHqlString( sb ); } if ( whereClause != null && whereClause.getPredicate() != null ) { sb.append( " where " ); @@ -625,84 +601,6 @@ public void appendHqlString(StringBuilder sb) { super.appendHqlString( sb ); } - private void appendJoins(SqmFrom sqmFrom, StringBuilder sb) { - for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { - switch ( sqmJoin.getSqmJoinType() ) { - case LEFT: - sb.append( " left join " ); - break; - case RIGHT: - sb.append( " right join " ); - break; - case INNER: - sb.append( " join " ); - break; - case FULL: - sb.append( " full join " ); - break; - case CROSS: - sb.append( " cross join " ); - break; - } - if ( sqmJoin instanceof SqmAttributeJoin ) { - final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( sqmFrom instanceof SqmTreatedPath ) { - final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; - sb.append( "treat(" ); - sb.append( treatedPath.getWrappedPath().resolveAlias() ); - sb.append( " as " ).append( treatedPath.getTreatTarget().getName() ).append( ')' ); - } - else { - sb.append( sqmFrom.resolveAlias() ); - } - sb.append( '.' ).append( ( attributeJoin ).getAttribute().getName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( attributeJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - attributeJoin.getJoinPredicate().appendHqlString( sb ); - } - appendJoins( sqmJoin, sb ); - } - else if ( sqmJoin instanceof SqmCrossJoin ) { - sb.append( ( (SqmCrossJoin) sqmJoin ).getEntityName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - appendJoins( sqmJoin, sb ); - } - else if ( sqmJoin instanceof SqmEntityJoin ) { - final SqmEntityJoin sqmEntityJoin = (SqmEntityJoin) sqmJoin; - sb.append( ( sqmEntityJoin ).getEntityName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( sqmEntityJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - sqmEntityJoin.getJoinPredicate().appendHqlString( sb ); - } - appendJoins( sqmJoin, sb ); - } - else { - throw new UnsupportedOperationException( "Unsupported join: " + sqmJoin ); - } - } - } - - private void appendJoins(SqmFrom sqmFrom, String correlationPrefix, StringBuilder sb) { - String separator = ""; - for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { - assert sqmJoin instanceof SqmAttributeJoin; - sb.append( separator ); - sb.append( correlationPrefix ).append( '.' ); - sb.append( ( (SqmAttributeJoin) sqmJoin ).getAttribute().getName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - appendJoins( sqmJoin, sb ); - separator = ", "; - } - } - - private void appendTreatJoins(SqmFrom sqmFrom, StringBuilder sb) { - for ( SqmFrom sqmTreat : sqmFrom.getSqmTreats() ) { - appendJoins( sqmTreat, sb ); - } - } - @Internal public boolean groupByClauseContains(NavigablePath navigablePath, SqmToSqlAstConverter sqlAstConverter) { if ( groupByClauseExpressions.isEmpty() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupJoinHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupJoinHelper.java new file mode 100644 index 000000000000..cfbdc3c52795 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupJoinHelper.java @@ -0,0 +1,46 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.ast.internal; + +import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.VirtualTableGroup; + +public class TableGroupJoinHelper { + + /** + * Determine the {@link TableGroupJoin} to which a custom {@code ON} clause predicate should be applied to. + * This is supposed to be called right after construction of a {@link TableGroupJoin}. + * This should also be called after a {@link org.hibernate.query.sqm.tree.predicate.SqmPredicate} is translated to a + * {@link org.hibernate.sql.ast.tree.predicate.Predicate}, because that translation might cause nested joins to be + * added to the table group of the join. + */ + public static TableGroupJoin determineJoinForPredicateApply(TableGroupJoin mainTableGroupJoin) { + final TableGroup mainTableGroup = mainTableGroupJoin.getJoinedGroup(); + if ( !mainTableGroup.getNestedTableGroupJoins().isEmpty() || mainTableGroup.getTableGroupJoins().isEmpty() ) { + // Always apply a predicate on the main table group join if it has nested table group joins or no joins + return mainTableGroupJoin; + } + else { + // If the main table group has just regular table group joins, + // prefer to apply predicates on the last table group join + final TableGroupJoin lastTableGroupJoin = mainTableGroup.getTableGroupJoins() + .get( mainTableGroup.getTableGroupJoins().size() - 1 ); + if ( lastTableGroupJoin.getJoinedGroup().getModelPart() instanceof EmbeddedCollectionPart ) { + // If the table group join refers to an embedded collection part, + // then the underlying table group *is* the main table group. + // Applying predicates on the join referring to the virtual table group would be a problem though, + // because these predicates will never be rendered. So use the main table group join in that case + assert lastTableGroupJoin.getJoinedGroup() instanceof VirtualTableGroup + && ( (VirtualTableGroup) lastTableGroupJoin.getJoinedGroup() ).getUnderlyingTableGroup() == mainTableGroup; + return mainTableGroupJoin; + } + return lastTableGroupJoin; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java index 72aaaed55477..f6bc7969c849 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.inheritance; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; @@ -23,6 +24,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -75,6 +78,95 @@ public void testTreatedJoin(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner2) as own1 on own1.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 on own1.description is null " + + "join treat(p.owner AS ProductOwner2) as own2 on own2.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttribute(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 " + + "join treat(p.owner AS ProductOwner2) as own2", + Product.class + ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttributeCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + p.join( "owner" ).treatAs( ProductOwner1.class ); + p.join( "owner" ).treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + final var ownerJoin = p.join( "owner" ); + ownerJoin.treatAs( ProductOwner1.class ); + ownerJoin.treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // The owner attribute is inner joined, but since there are multiple subtype treats, + // the type restriction for the treat usage does not filter rows + assertThat( result ).hasSize( 2 ); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + @Test public void testImplicitTreatedJoin(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/LeftJoinFetchSubclassesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/LeftJoinFetchSubclassesTest.java new file mode 100644 index 000000000000..6b9485a741b9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/LeftJoinFetchSubclassesTest.java @@ -0,0 +1,175 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@SessionFactory +@DomainModel( annotatedClasses = { + LeftJoinFetchSubclassesTest.Entity1.class, + LeftJoinFetchSubclassesTest.SuperClass.class, + LeftJoinFetchSubclassesTest.SubClass1.class, + LeftJoinFetchSubclassesTest.SubClass2.class, +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16798" ) +public class LeftJoinFetchSubclassesTest { + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Entity1 entity1A = new Entity1( 1L ); + session.persist( entity1A ); + final SubClass1 subClass1 = new SubClass1(); + subClass1.setId( 2L ); + subClass1.setEntity1( entity1A ); + session.persist( subClass1 ); + final Entity1 entity1B = new Entity1( 3L ); + session.persist( entity1B ); + final SubClass2 subClass2 = new SubClass2(); + subClass2.setId( 4L ); + subClass2.setEntity1( entity1B ); + session.persist( subClass2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from SuperClass" ).executeUpdate(); + session.createMutationQuery( "delete from Entity1" ).executeUpdate(); + } ); + } + + @Test + public void testJoinFetchSub1(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Entity1 entity1 = session.createQuery( + "select e from Entity1 e left join fetch e.subClass1 where e.id = 1", + Entity1.class + ).getSingleResult(); + assertThat( entity1.getSubClass1().getId() ).isEqualTo( 2L ); + assertThat( entity1.getSubClass2() ).isNull(); + } ); + } + + @Test + public void testJoinFetchSub2(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Entity1 entity1 = session.createQuery( + "select e from Entity1 e left join fetch e.subClass2 where e.id = 3", + Entity1.class + ).getSingleResult(); + assertThat( entity1.getSubClass1() ).isNull(); + assertThat( entity1.getSubClass2().getId() ).isEqualTo( 4L ); + } ); + } + + @Test + public void testJoinFetchBoth(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select e from Entity1 e left join fetch e.subClass1 left join fetch e.subClass2", + Entity1.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + } ); + } + + @Test + public void testJoinBoth(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select e from Entity1 e left join e.subClass1 left join e.subClass2", + Entity1.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + } ); + } + + @Entity( name = "Entity1" ) + public static class Entity1 { + @Id + private Long id; + + public Entity1() { + } + + public Entity1(Long id) { + this.id = id; + } + + @OneToOne( fetch = FetchType.LAZY, mappedBy = "entity1" ) + private SubClass1 subClass1; + + @OneToOne( fetch = FetchType.LAZY, mappedBy = "entity1" ) + private SubClass2 subClass2; + + public SubClass1 getSubClass1() { + return subClass1; + } + + public SubClass2 getSubClass2() { + return subClass2; + } + } + + @Entity( name = "SuperClass" ) + @Inheritance( strategy = InheritanceType.SINGLE_TABLE ) + public static abstract class SuperClass { + @Id + private Long id; + + @OneToOne + private Entity1 entity1; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Entity1 getEntity1() { + return entity1; + } + + public void setEntity1(Entity1 entity1) { + this.entity1 = entity1; + } + } + + @Entity( name = "SubClass1" ) + @DiscriminatorValue( "1" ) + public static class SubClass1 extends SuperClass { + } + + @Entity( name = "SubClass2" ) + @DiscriminatorValue( "2" ) + public static class SubClass2 extends SuperClass { + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java new file mode 100644 index 000000000000..cf4e8bef8c99 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java @@ -0,0 +1,345 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.ManyToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( annotatedClasses = { + ManyToManyTreatJoinTest.ParentEntity.class, + ManyToManyTreatJoinTest.SingleBase.class, + ManyToManyTreatJoinTest.SingleSub1.class, + ManyToManyTreatJoinTest.SingleSub2.class, + ManyToManyTreatJoinTest.JoinedBase.class, + ManyToManyTreatJoinTest.JoinedSub1.class, + ManyToManyTreatJoinTest.JoinedSub2.class, + ManyToManyTreatJoinTest.UnionBase.class, + ManyToManyTreatJoinTest.UnionSub1.class, + ManyToManyTreatJoinTest.UnionSub2.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +public class ManyToManyTreatJoinTest { + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final ParentEntity parent1 = new ParentEntity( 1 ); + parent1.getSingleEntities().add( new SingleSub1( 2, 2 ) ); + parent1.getJoinedEntities().add( new JoinedSub1( 3, 3 ) ); + parent1.getUnionEntities().put( 4, new UnionSub1( 4, 4 ) ); + session.persist( parent1 ); + final ParentEntity parent2 = new ParentEntity( 5 ); + parent2.getSingleEntities().add( new SingleSub2( 6 ) ); + parent2.getJoinedEntities().add( new JoinedSub2( 7 ) ); + parent2.getUnionEntities().put( 8, new UnionSub2( 8 ) ); + session.persist( parent2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createQuery( "from ParentEntity", ParentEntity.class ) + .getResultList() + .forEach( session::remove ) ); + } + + @Test + public void testSingleTableSelectChild(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.singleEntities as SingleSub1) s", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 2 ); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + public void testSingleTableSelectParent(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select p.id from ParentEntity p join treat(p.singleEntities as SingleSub1) s", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1 ); + // We always join the element table to restrict to the correct subtype + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testSingleTableTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.singleEntities as SingleSub1) s on s.subProp<>2", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + public void testJoinedSelectChild(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select j.subProp from ParentEntity p join treat(p.joinedEntities as JoinedSub1) j", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 3 ); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + + @Test + public void testJoinedSelectParent(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select p.id from ParentEntity p join treat(p.joinedEntities as JoinedSub1) j", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1 ); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testJoinedTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.joinedEntities as JoinedSub1) s on s.subProp<>3", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + + @Test + public void testTablePerClassSelectChild(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select u.subProp from ParentEntity p join treat(p.unionEntities as UnionSub1) u", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 4 ); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + public void testTablePerClassSelectParent(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select p.id from ParentEntity p join treat(p.unionEntities as UnionSub1) u", + Integer.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1 ); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTablePerClassTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.unionEntities as UnionSub1) s on s.subProp<>4", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Entity( name = "ParentEntity" ) + public static class ParentEntity { + @Id + private Integer id; + + @ManyToMany( cascade = CascadeType.PERSIST ) + private List singleEntities = new ArrayList<>(); + + @ManyToMany( cascade = CascadeType.PERSIST ) + private Set joinedEntities = new HashSet<>(); + + @ManyToMany( cascade = CascadeType.PERSIST ) + private Map unionEntities = new HashMap<>(); + + public ParentEntity() { + } + + public ParentEntity(Integer id) { + this.id = id; + } + + public List getSingleEntities() { + return singleEntities; + } + + public Set getJoinedEntities() { + return joinedEntities; + } + + public Map getUnionEntities() { + return unionEntities; + } + } + + @Entity( name = "SingleBase" ) + @Inheritance( strategy = InheritanceType.SINGLE_TABLE ) + public static abstract class SingleBase { + @Id + private Integer id; + + public SingleBase() { + } + + public SingleBase(Integer id) { + this.id = id; + } + } + + @Entity( name = "SingleSub1" ) + public static class SingleSub1 extends SingleBase { + private Integer subProp; + + public SingleSub1() { + } + + public SingleSub1(Integer id, Integer subProp) { + super( id ); + this.subProp = subProp; + } + } + + @Entity( name = "SingleSub2" ) + public static class SingleSub2 extends SingleBase { + public SingleSub2() { + } + + public SingleSub2(Integer id) { + super( id ); + } + } + + @Entity( name = "JoinedBase" ) + @Inheritance( strategy = InheritanceType.JOINED ) + public static abstract class JoinedBase { + @Id + private Integer id; + + public JoinedBase() { + } + + public JoinedBase(Integer id) { + this.id = id; + } + } + + @Entity( name = "JoinedSub1" ) + public static class JoinedSub1 extends JoinedBase { + private Integer subProp; + + public JoinedSub1() { + } + + public JoinedSub1(Integer id, Integer subProp) { + super( id ); + this.subProp = subProp; + } + } + + @Entity( name = "JoinedSub2" ) + public static class JoinedSub2 extends JoinedBase { + public JoinedSub2() { + } + + public JoinedSub2(Integer id) { + super( id ); + } + } + + @Entity( name = "UnionBase" ) + @Inheritance( strategy = InheritanceType.TABLE_PER_CLASS ) + public static abstract class UnionBase { + @Id + private Integer id; + + public UnionBase() { + } + + public UnionBase(Integer id) { + this.id = id; + } + } + + @Entity( name = "UnionSub1" ) + public static class UnionSub1 extends UnionBase { + private Integer subProp; + + public UnionSub1() { + } + + public UnionSub1(Integer id, Integer subProp) { + super( id ); + this.subProp = subProp; + } + } + + @Entity( name = "UnionSub2" ) + public static class UnionSub2 extends UnionBase { + public UnionSub2() { + } + + public UnionSub2(Integer id) { + super( id ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java new file mode 100644 index 000000000000..ce2d962d4b14 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java @@ -0,0 +1,397 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + AttributeJoinWithJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class AttributeJoinWithJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) + @Disabled("Can't backport improvement to 6.2 that changes SPIs") + public void testLeftJoinSelectFk(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select ce.id " + + "from RootOne r left join r.child ce ", + Integer.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertEquals( 11, resultList.get( 0 ) ); + assertNull( resultList.get( 1 ) ); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join treat(r.child as ChildEntityA) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join r.child ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id" ) + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java new file mode 100644 index 000000000000..16b40be721bb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java @@ -0,0 +1,259 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithNaturalJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithNaturalJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithNaturalJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + @Disabled("Can't backport improvement to 6.2 that changes SPIs") + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r left join treat(r.child as SubChildEntityA1) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass, + Integer uk) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + if ( uk != null ) { + assertEquals( uk, result.get( 2 ) ); + } + else { + assertNull( result.get( 2 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + @Column(unique = true) + private Integer uk; + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + this.uk = id; + } + + public Integer getUk() { + return uk; + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id", referencedColumnName = "uk") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java new file mode 100644 index 000000000000..3830961792ef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java @@ -0,0 +1,247 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.annotations.Where; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithRestrictedJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithRestrictedJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createNativeQuery( "delete from root_one" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createNativeQuery( "delete from child_entity" ).executeUpdate(); + s.createNativeQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + @Where( clause = "ident < 20" ) + public static class BaseClass { + @Id + @Column(name = "ident") + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java new file mode 100644 index 000000000000..1ecf1e7b1db4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java @@ -0,0 +1,360 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + AttributeJoinWithSingleTableInheritanceTest.BaseClass.class, + AttributeJoinWithSingleTableInheritanceTest.ChildEntityA.class, + AttributeJoinWithSingleTableInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithSingleTableInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithSingleTableInheritanceTest.ChildEntityB.class, + AttributeJoinWithSingleTableInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class AttributeJoinWithSingleTableInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join treat(r.child as ChildEntityA) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join r.child ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.SINGLE_TABLE ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id" ) + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java new file mode 100644 index 000000000000..dcfd5b757730 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java @@ -0,0 +1,346 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + AttributeJoinWithTablePerClassInheritanceTest.BaseClass.class, + AttributeJoinWithTablePerClassInheritanceTest.ChildEntityA.class, + AttributeJoinWithTablePerClassInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithTablePerClassInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithTablePerClassInheritanceTest.ChildEntityB.class, + AttributeJoinWithTablePerClassInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class AttributeJoinWithTablePerClassInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null ); + } ); + } + + @Test + public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join treat(r.child as ChildEntityA) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, SubChildEntityA1.class ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join r.child ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final SubChildEntityA1 subChildEntityA1 = new SubChildEntityA1( 11 ); + s.persist( subChildEntityA1 ); + final SubChildEntityA2 subChildEntityA2 = new SubChildEntityA2( 12 ); + s.persist( subChildEntityA2 ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, subChildEntityA1 ) ); + s.persist( new RootOne( 2, subChildEntityA1 ) ); + s.persist( new RootOne( 3, null ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 3" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.TABLE_PER_CLASS ) + public static class BaseClass { + @Id + private Integer id; + private String name; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id" ) + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithJoinedInheritanceTest.java new file mode 100644 index 000000000000..af280f6efb8b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithJoinedInheritanceTest.java @@ -0,0 +1,290 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + EntityJoinWithJoinedInheritanceTest.BaseClass.class, + EntityJoinWithJoinedInheritanceTest.ChildEntityA.class, + EntityJoinWithJoinedInheritanceTest.SubChildEntityA1.class, + EntityJoinWithJoinedInheritanceTest.SubChildEntityA2.class, + EntityJoinWithJoinedInheritanceTest.ChildEntityB.class, + EntityJoinWithJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16438" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class EntityJoinWithJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testSimpleLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> s.persist( new RootOne( 1, null ) ) ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r from RootOne r left join ChildEntityA ce on ce.id = r.childId", + RootOne.class + ).getResultList(); + assertEquals( 1, resultList.size() ); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join ChildEntityA ce on ce.id = r.childId " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + public static class RootOne { + @Id + private Integer id; + private Integer childId; + + public RootOne() { + } + + public RootOne(Integer id, Integer childId) { + this.id = id; + this.childId = childId; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithSingleTableInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithSingleTableInheritanceTest.java new file mode 100644 index 000000000000..8b192aa32dc9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithSingleTableInheritanceTest.java @@ -0,0 +1,281 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Jan Schatteman + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + EntityJoinWithSingleTableInheritanceTest.BaseClass.class, + EntityJoinWithSingleTableInheritanceTest.ChildEntityA.class, + EntityJoinWithSingleTableInheritanceTest.SubChildEntityA1.class, + EntityJoinWithSingleTableInheritanceTest.SubChildEntityA2.class, + EntityJoinWithSingleTableInheritanceTest.ChildEntityB.class, + EntityJoinWithSingleTableInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16438" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class EntityJoinWithSingleTableInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testSimpleLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> s.persist( new RootOne( 1, null ) ) ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r from RootOne r left join ChildEntityA ce on ce.id = r.childId", + RootOne.class + ).getResultList(); + assertEquals( 1, resultList.size() ); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join ChildEntityA ce on ce.id = r.childId " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, "child_a_2", SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.SINGLE_TABLE ) + @DiscriminatorColumn( name = "disc_col", discriminatorType = DiscriminatorType.STRING ) + public static class BaseClass { + @Id + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + public static class RootOne { + @Id + private Integer id; + private Integer childId; + + public RootOne() { + } + + public RootOne(Integer id, Integer childId) { + this.id = id; + this.childId = childId; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithTablePerClassInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithTablePerClassInheritanceTest.java new file mode 100644 index 000000000000..344b27c0ca1a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/EntityJoinWithTablePerClassInheritanceTest.java @@ -0,0 +1,265 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + EntityJoinWithTablePerClassInheritanceTest.BaseClass.class, + EntityJoinWithTablePerClassInheritanceTest.ChildEntityA.class, + EntityJoinWithTablePerClassInheritanceTest.SubChildEntityA1.class, + EntityJoinWithTablePerClassInheritanceTest.SubChildEntityA2.class, + EntityJoinWithTablePerClassInheritanceTest.ChildEntityB.class, + EntityJoinWithTablePerClassInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-16438" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16494" ) +public class EntityJoinWithTablePerClassInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testSimpleLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> s.persist( new RootOne( 1, null ) ) ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r from RootOne r left join ChildEntityA ce on ce.id = r.childId", + RootOne.class + ).getResultList(); + assertEquals( 1, resultList.size() ); + } ); + } + + @Test + public void testLeftJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join ChildEntityA ce on ce.id = r.childId " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + // r2 has to be there, but shouldn't have any data w/ respect to ChildEntityB + assertResult( resultList.get( 1 ), 2, 21, null, null ); + } ); + } + + @Test + public void testRightJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r right join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 3, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), null, null, 12, SubChildEntityA2.class ); + } ); + } + + @Test + public void testCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 21 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r cross join ChildEntityA ce " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, 11, SubChildEntityA1.class ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoin(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.persist( new SubChildEntityA1( 11 ) ); + s.persist( new SubChildEntityA2( 12 ) ); + s.persist( new ChildEntityB( 21 ) ); + s.persist( new RootOne( 1, 11 ) ); + s.persist( new RootOne( 2, 11 ) ); + s.persist( new RootOne( 3, 21 ) ); + s.persist( new RootOne( 4, null ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r full join ChildEntityA ce on ce.id = r.childId " + + "order by r.id nulls last, ce.id", + Tuple.class + ).getResultList(); + assertEquals( 5, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 11, 11, SubChildEntityA1.class ); + assertResult( resultList.get( 2 ), 3, 21, null, null ); + assertResult( resultList.get( 3 ), 4, null, null, null ); + assertResult( resultList.get( 4 ), null, null, 12, SubChildEntityA2.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.TABLE_PER_CLASS ) + public static class BaseClass { + @Id + private Integer id; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + } + + @Entity( name = "ChildEntityA" ) + public static abstract class ChildEntityA extends BaseClass { + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + public static class RootOne { + @Id + private Integer id; + private Integer childId; + + public RootOne() { + } + + public RootOne(Integer id, Integer childId) { + this.id = id; + this.childId = childId; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinWithSingleTableInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinWithSingleTableInheritanceTest.java deleted file mode 100644 index c0e5ff006c10..000000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinWithSingleTableInheritanceTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.test.join; - -import java.util.List; - -import jakarta.persistence.Column; -import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.DiscriminatorType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import org.hibernate.testing.TestForIssue; -import org.hibernate.testing.orm.junit.DialectFeatureChecks; -import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.RequiresDialectFeature; -import org.hibernate.testing.orm.junit.SessionFactory; -import org.hibernate.testing.orm.junit.SessionFactoryScope; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -/** - * @author Jan Schatteman - */ -@TestForIssue( jiraKey = "HHH-16435") -@DomainModel( - annotatedClasses = { - JoinWithSingleTableInheritanceTest.AbstractSuperClass.class, - JoinWithSingleTableInheritanceTest.ChildEntityA.class, - JoinWithSingleTableInheritanceTest.ChildEntityB.class, - JoinWithSingleTableInheritanceTest.RootOne.class - } -) -@SessionFactory -public class JoinWithSingleTableInheritanceTest { - - @AfterEach - public void cleanup( SessionFactoryScope scope ) { - scope.inTransaction( - s -> { - s.createMutationQuery( "delete from RootOne" ).executeUpdate(); - s.createMutationQuery( "delete from AbstractSuperClass" ).executeUpdate(); - } - ); - } - - @Test - public void testLeftJoinOnSingleTableInheritance(SessionFactoryScope scope) { - scope.inTransaction( - s -> s.persist( new RootOne(1) ) - ); - - scope.inTransaction( - s -> { - List l = s.createSelectionQuery( "select r from RootOne r left join ChildEntityA ce on ce.id = r.someOtherId", RootOne.class ).list(); - assertEquals( 1, l.size() ); - } - ); - } - - @Test - public void testLeftJoinOnSingleTableInheritance2(SessionFactoryScope scope) { - scope.inTransaction( - s -> { - s.persist(new ChildEntityA( 11 )); - s.persist(new ChildEntityB( 21 )); - RootOne r1 = new RootOne(1); - r1.setSomeOtherId( 11 ); - s.persist( r1 ); - RootOne r2 = new RootOne(2); - r2.setSomeOtherId( 21 ); - s.persist( r2 ); - } - ); - - scope.inTransaction( - s -> { - List l = s.createSelectionQuery( "select r.id, r.someOtherId, ce.id, ce.disc_col from RootOne r left join ChildEntityA ce on ce.id = r.someOtherId order by r.id", Object[].class ).list(); - assertEquals( 2, l.size() ); - Object[] r1 = l.get( 0 ); - assertEquals( 1, (int) r1[0] ); - assertEquals( 11, (int) r1[1] ); - assertEquals( 11, (int) r1[2] ); - assertEquals( 1, (int) r1[3] ); - // r2 has to be there, but shouldn't have any data w/ respect to ChildEntityB - Object[] r2 = l.get( 1 ); - assertEquals( 2, (int) r2[0] ); - assertEquals( 21, (int) r2[1] ); - assertNull( r2[2] ); - assertNull( r2[3] ); - } - ); - } - - @Test - @Disabled(value = "HHH-16494") - public void testRightJoinOnSingleTableInheritance(SessionFactoryScope scope) { - scope.inTransaction( - s -> { - s.persist(new ChildEntityA( 11 )); - s.persist(new ChildEntityA( 12 )); - s.persist(new ChildEntityB( 21 )); - RootOne r1 = new RootOne(1); - r1.setSomeOtherId( 11 ); - s.persist( r1 ); - RootOne r2 = new RootOne(2); - r2.setSomeOtherId( 11 ); - s.persist( r2 ); - RootOne r3 = new RootOne(3); - r3.setSomeOtherId( 21 ); - s.persist( r3 ); - } - ); - - scope.inTransaction( - s -> { - List l = s.createSelectionQuery( "select r.id, r.someOtherId, ce.id, ce.disc_col from RootOne r right join ChildEntityA ce on ce.id = r.someOtherId order by ce.id, r.id", Object[].class ).list(); - assertEquals( 3, l.size() ); - Object[] r1 = l.get( 0 ); - assertEquals( 1, (int) r1[0] ); - assertEquals( 11, (int) r1[1] ); - assertEquals( 11, (int) r1[2] ); - assertEquals( 1, (int) r1[3] ); - Object[] r2 = l.get( 1 ); - assertEquals( 2, (int) r2[0] ); - assertEquals( 11, (int) r2[1] ); - assertEquals( 11, (int) r2[2] ); - assertEquals( 1, (int) r2[3] ); - Object[] r3 = l.get( 2 ); - assertNull( r3[0] ); - assertNull( r3[1] ); - assertEquals( 12, (int) r3[2] ); - assertEquals( 1, (int) r3[3] ); - } - ); - } - - @Test - @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFullJoin.class) - @Disabled(value = "HHH-16494") - public void testFullJoinOnSingleTableInheritance(SessionFactoryScope scope) { - scope.inTransaction( - s -> { - s.persist(new ChildEntityA( 11 )); - s.persist(new ChildEntityA( 12 )); - s.persist(new ChildEntityB( 21 )); - s.persist(new ChildEntityB( 22 )); - RootOne r1 = new RootOne(1); - r1.setSomeOtherId( 11 ); - s.persist( r1 ); - RootOne r2 = new RootOne(2); - r2.setSomeOtherId( 11 ); - s.persist( r2 ); - RootOne r3 = new RootOne(3); - r3.setSomeOtherId( 21 ); - s.persist( r3 ); - RootOne r4 = new RootOne(4); - s.persist( r4 ); - } - ); - - scope.inTransaction( - s -> { - List l = s.createSelectionQuery( "select r.id, r.someOtherId from RootOne r full join ChildEntityA ce on ce.id = r.someOtherId order by ce.id, r.id", Object[].class ).list(); - assertEquals( 7, l.size() ); - } - ); - } - - @Entity(name = "AbstractSuperClass") - @DiscriminatorColumn(name = "disc_col", discriminatorType = DiscriminatorType.INTEGER) - public static abstract class AbstractSuperClass { - @Id - private Integer id; - - @Column(insertable = false, updatable = false) - private Integer disc_col; - - public AbstractSuperClass(Integer id) { - this.id = id; - } - } - - @Entity(name = "ChildEntityA") - @DiscriminatorValue("1") - public static class ChildEntityA extends AbstractSuperClass { - public ChildEntityA(Integer id) { - super( id ); - } - } - - @Entity(name = "ChildEntityB") - @DiscriminatorValue("2") - public static class ChildEntityB extends AbstractSuperClass { - public ChildEntityB(Integer id) { - super( id ); - } - } - - @Entity(name = "RootOne") - public static class RootOne { - @Id - Integer id; - Integer someOtherId; - - public RootOne() { - } - - public RootOne(Integer id) { - this.id = id; - } - - public Integer getSomeOtherId() { - return someOtherId; - } - - public void setSomeOtherId(Integer someOtherId) { - this.someOtherId = someOtherId; - } - } - -} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/onetomany/OneToManyBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/onetomany/OneToManyBidirectionalTest.java index 26cef8c9ff77..0cffa5b2cfe1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/onetomany/OneToManyBidirectionalTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/onetomany/OneToManyBidirectionalTest.java @@ -93,15 +93,9 @@ public void testFetchingSameAssociationTwice(SessionFactoryScope scope) { List items = session.createQuery( "from Item i" + " join fetch i.order o" + - " join fetch i.order o2", Item.class ).list(); - /* - select i1_0.id, o21_0.id, o21_0.name - from Item as i1_0 - inner join "Order" as o21_0 on i1_0."order_id" = o21_0.id - inner join "Order" as o1_0 on i1_0."order_id" = o1_0.id - */ + " join fetch i.order", Item.class ).list(); - sqlStatementInterceptor.assertNumberOfJoins( 0, SqlAstJoinType.INNER, 2 ); + sqlStatementInterceptor.assertNumberOfJoins( 0, SqlAstJoinType.INNER, 1 ); sqlStatementInterceptor.clear(); assertThat( items.size(), is( 2 ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ToOneMultipleFetchesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ToOneMultipleFetchesTest.java new file mode 100644 index 000000000000..0981200d960a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ToOneMultipleFetchesTest.java @@ -0,0 +1,255 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.hql; + +import org.hibernate.Hibernate; +import org.hibernate.query.sqm.InterpretationException; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + ToOneMultipleFetchesTest.EntityA.class, + ToOneMultipleFetchesTest.EntityB.class, + ToOneMultipleFetchesTest.EntityC.class, + ToOneMultipleFetchesTest.EntityD.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17777" ) +public class ToOneMultipleFetchesTest { + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityC entityC = new EntityC( 1L, "entity_c" ); + session.persist( entityC ); + final EntityD entityD = new EntityD( 2L, "entity_d" ); + session.persist( entityD ); + final EntityB entityB2 = new EntityB( 3L, entityC, entityD ); + session.persist( entityB2 ); + session.persist( new EntityA( 4L, entityB2 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + session.createMutationQuery( "delete from EntityB" ).executeUpdate(); + session.createMutationQuery( "delete from EntityC" ).executeUpdate(); + session.createMutationQuery( "delete from EntityD" ).executeUpdate(); + } ); + } + + @Test + public void testCriteriaMultipleFetches(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery query = builder.createQuery( EntityA.class ); + final Root root = query.from( EntityA.class ); + root.fetch( "entityB", JoinType.INNER ).fetch( "entityC", JoinType.INNER ); + root.fetch( "entityB", JoinType.INNER ).fetch( "entityD", JoinType.INNER ); + final EntityA result = session.createQuery( query ).getSingleResult(); + assertThat( Hibernate.isInitialized( result.getEntityB() ) ).isTrue(); + final EntityB entityB = result.getEntityB(); + assertThat( Hibernate.isInitialized( entityB.getEntityC() ) ).isTrue(); + assertThat( Hibernate.isInitialized( entityB.getEntityD() ) ).isTrue(); + } ); + } + + @Test + public void testCriteriaFetchReuse(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery query = builder.createQuery( EntityA.class ); + final Root root = query.from( EntityA.class ); + final Fetch fetchEntityB = root.fetch( "entityB", JoinType.INNER ); + fetchEntityB.fetch( "entityC", JoinType.INNER ); + fetchEntityB.fetch( "entityD", JoinType.INNER ); + final EntityA result = session.createQuery( query ).getSingleResult(); + assertThat( Hibernate.isInitialized( result.getEntityB() ) ).isTrue(); + final EntityB entityB = result.getEntityB(); + assertThat( Hibernate.isInitialized( entityB.getEntityD() ) ).isTrue(); + assertThat( Hibernate.isInitialized( entityB.getEntityC() ) ).isTrue(); + } ); + } + + @Test + public void testMultipleFetchesError(SessionFactoryScope scope) { + scope.inSession( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery query = builder.createQuery( EntityA.class ); + final Root root = query.from( EntityA.class ); + root.fetch( "entityB", JoinType.LEFT ); + try { + root.fetch( "entityB", JoinType.INNER ); + fail( "Multiple fetches with different join types should not be allowed" ); + } + catch (Exception e) { + assertThat( e ) + .isInstanceOf( IllegalStateException.class ) + .hasMessage( + "Requested join fetch with association [entityB] with 'inner' join type, " + + "but found existing join fetch with 'left outer' join type." + ); + } + } ); + + scope.inSession( session -> { + try { + session.createQuery( + "from EntityA a " + + "join fetch a.entityB b1 join fetch b1.entityC " + + "join fetch a.entityB b2 join fetch b2.entityD", + EntityA.class + ); + } + catch (Exception e) { + assertThat( e ) + .isInstanceOf( IllegalArgumentException.class ) + .getCause() + .isInstanceOf( InterpretationException.class ) + .getCause() + .isInstanceOf( IllegalStateException.class ) + .hasMessage( "Cannot fetch the same association twice with a different alias" ); + } + } ); + } + + @Test + public void testHQLMultipleFetches(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA result = session.createQuery( + "from EntityA a " + + "join fetch a.entityB b1 join fetch b1.entityC " + + "join fetch a.entityB join fetch a.entityB.entityD", + EntityA.class + ).getSingleResult(); + assertThat( Hibernate.isInitialized( result.getEntityB() ) ).isTrue(); + final EntityB entityB = result.getEntityB(); + assertThat( Hibernate.isInitialized( entityB.getEntityD() ) ).isTrue(); + assertThat( Hibernate.isInitialized( entityB.getEntityC() ) ).isTrue(); + } ); + } + + @Test + public void testHQLFetchReuse(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA result = session.createQuery( + "from EntityA a " + + "join fetch a.entityB b left join fetch b.entityC join fetch b.entityD", + EntityA.class + ).getSingleResult(); + assertThat( Hibernate.isInitialized( result.getEntityB() ) ).isTrue(); + final EntityB entityB = result.getEntityB(); + assertThat( Hibernate.isInitialized( entityB.getEntityD() ) ).isTrue(); + assertThat( Hibernate.isInitialized( entityB.getEntityC() ) ).isTrue(); + } ); + } + + @Entity( name = "EntityA" ) + public static class EntityA { + @Id + private Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(Long id, EntityB entityB) { + this.id = id; + this.entityB = entityB; + } + + public EntityB getEntityB() { + return entityB; + } + } + + @Entity( name = "EntityB" ) + public static class EntityB { + @Id + private Long id; + + @ManyToOne + private EntityC entityC; + + @ManyToOne( fetch = FetchType.LAZY ) + private EntityD entityD; + + public EntityB() { + } + + public EntityB(Long id, EntityC entityC, EntityD entityD) { + this.id = id; + this.entityC = entityC; + this.entityD = entityD; + } + + public EntityD getEntityD() { + return entityD; + } + + public EntityC getEntityC() { + return entityC; + } + } + + @Entity( name = "EntityC" ) + public static class EntityC { + @Id + private Long id; + + private String name; + + public EntityC() { + } + + public EntityC(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity( name = "EntityD" ) + public static class EntityD { + @Id + private Long id; + + private String name; + + public EntityD() { + } + + public EntityD(Long id, String name) { + this.id = id; + this.name = name; + } + } +}