diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java index 93644cfff5..fe2d2d5cd5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java @@ -124,7 +124,7 @@ public class SQLServerLegacySqlAstTranslator extends Ab appendSql( UNION_ALL ); searchIndex = unionIndex + UNION_ALL.length(); } - append( tableExpression, searchIndex, tableExpression.length() - 2 ); + append( tableExpression, searchIndex, tableExpression.length() - 1 ); renderLockHint( lockMode ); appendSql( " )" ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java index 5fe79071da..893cb493be 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java @@ -1093,7 +1093,7 @@ public class BinderHelper { return; } else { - ownerClass = getSuperPersistentClass( ownerClass ); + ownerClass = ownerClass.getSuperPersistentClass(); } } throw new AnnotationException( @@ -1110,24 +1110,9 @@ public class BinderHelper { if ( ownerClass.getTable() == referencedClass.getTable() ) { return true; } - referencedClass = getSuperPersistentClass( referencedClass ); + referencedClass = referencedClass.getSuperPersistentClass(); } return false; } - private static PersistentClass getSuperPersistentClass(PersistentClass persistentClass) { - return persistentClass.getSuperclass() != null ? persistentClass.getSuperclass() - : getSuperPersistentClass( persistentClass.getSuperMappedSuperclass() ); - } - - private static PersistentClass getSuperPersistentClass(MappedSuperclass mappedSuperclass) { - if ( mappedSuperclass != null ) { - final PersistentClass superClass = mappedSuperclass.getSuperPersistentClass(); - if ( superClass != null ) { - return superClass; - } - return getSuperPersistentClass( mappedSuperclass.getSuperMappedSuperclass() ); - } - return null; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java index f2a6d6c555..893fa1ccc2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java @@ -123,7 +123,7 @@ public class SQLServerSqlAstTranslator extends SqlAstTr appendSql( UNION_ALL ); searchIndex = unionIndex + UNION_ALL.length(); } - append( tableExpression, searchIndex, tableExpression.length() - 2 ); + append( tableExpression, searchIndex, tableExpression.length() - 1 ); renderLockHint( lockMode ); appendSql( " )" ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java index 2293eca816..71331dd138 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/FilterHelper.java @@ -19,6 +19,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.Restrictable; +import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.sql.Template; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -42,6 +43,11 @@ public class FilterHelper { private final boolean[] filterAutoAliasFlags; private final Map[] filterAliasTableMaps; private final List[] parameterNames; + private final Map tableToEntityName; + + public FilterHelper(List filters, SessionFactoryImplementor factory) { + this( filters, null, factory ); + } /** * The map of defined filters. This is expected to be in format @@ -51,7 +57,7 @@ public class FilterHelper { * @param filters The map of defined filters. * @param factory The session factory */ - public FilterHelper(List filters, SessionFactoryImplementor factory) { + public FilterHelper(List filters, Map tableToEntityName, SessionFactoryImplementor factory) { int filterCount = filters.size(); filterNames = new String[filterCount]; @@ -59,6 +65,7 @@ public class FilterHelper { filterAutoAliasFlags = new boolean[filterCount]; filterAliasTableMaps = new Map[filterCount]; parameterNames = new List[filterCount]; + this.tableToEntityName = tableToEntityName; filterCount = 0; for ( final FilterConfiguration filter : filters ) { @@ -145,21 +152,32 @@ public class FilterHelper { public void applyEnabledFilters( Consumer predicateConsumer, FilterAliasGenerator aliasGenerator, - Map enabledFilters) { - final FilterPredicate predicate = generateFilterPredicate( aliasGenerator, enabledFilters ); + Map enabledFilters, + TableGroup tableGroup, + SqlAstCreationState creationState) { + final FilterPredicate predicate = generateFilterPredicate( + aliasGenerator, + enabledFilters, + tableGroup, + creationState + ); if ( predicate != null ) { predicateConsumer.accept( predicate ); } } - private FilterPredicate generateFilterPredicate(FilterAliasGenerator aliasGenerator, Map enabledFilters) { + private FilterPredicate generateFilterPredicate( + FilterAliasGenerator aliasGenerator, + Map enabledFilters, + TableGroup tableGroup, + SqlAstCreationState creationState) { final FilterPredicate filterPredicate = new FilterPredicate(); for ( int i = 0, max = filterNames.length; i < max; i++ ) { final String filterName = filterNames[i]; final FilterImpl enabledFilter = (FilterImpl) enabledFilters.get( filterName ); if ( enabledFilter != null ) { - filterPredicate.applyFragment( render( aliasGenerator, i ), enabledFilter, parameterNames[i] ); + filterPredicate.applyFragment( render( aliasGenerator, i, tableGroup, creationState ), enabledFilter, parameterNames[i] ); } } @@ -187,34 +205,78 @@ public class FilterHelper { if ( buffer.length() > 0 ) { buffer.append( " and " ); } - buffer.append( render( aliasGenerator, i ) ); + buffer.append( render( aliasGenerator, i, null, null ) ); } } } } - private String render(FilterAliasGenerator aliasGenerator, int filterIndex) { + private String render( + FilterAliasGenerator aliasGenerator, + int filterIndex, + TableGroup tableGroup, + SqlAstCreationState creationState) { Map aliasTableMap = filterAliasTableMaps[filterIndex]; String condition = filterConditions[filterIndex]; if ( aliasGenerator == null ) { return StringHelper.replace( condition, FilterImpl.MARKER + ".", ""); } if ( filterAutoAliasFlags[filterIndex] ) { - return StringHelper.replace( + final String tableName = aliasTableMap.get( null ); + final String newCondition = StringHelper.replace( condition, FilterImpl.MARKER, - aliasGenerator.getAlias( aliasTableMap.get( null ) ) + aliasGenerator.getAlias( tableName ) ); + if ( creationState != null && tableToEntityName != null && !newCondition.equals( condition ) ) { + creationState.registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + tableToEntityName.get( + tableName == null + ? tableGroup.getPrimaryTableReference().getTableId() + : tableName + ) + ); + } + return newCondition; } else if ( isTableFromPersistentClass( aliasTableMap ) ) { - return StringHelper.replace( condition, "{alias}", aliasGenerator.getAlias( aliasTableMap.get( null ) ) ); + final String tableName = aliasTableMap.get( null ); + final String newCondition = StringHelper.replace( + condition, + "{alias}", + aliasGenerator.getAlias( tableName ) + ); + if ( creationState != null && !newCondition.equals( condition ) ) { + creationState.registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + tableToEntityName.get( + tableName == null + ? tableGroup.getPrimaryTableReference().getTableId() + : tableName + ) + ); + } + return newCondition; } else { for ( Map.Entry entry : aliasTableMap.entrySet() ) { - condition = StringHelper.replace( condition, + final String tableName = entry.getValue(); + final String newCondition = StringHelper.replace( + condition, "{" + entry.getKey() + "}", - aliasGenerator.getAlias( entry.getValue() ) + aliasGenerator.getAlias( tableName ) ); + if ( creationState != null && !newCondition.equals( condition ) ) { + creationState.registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + tableToEntityName.get( tableName ) + ); + } + condition = newCondition; } return condition; } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java index cf20eb9ed9..eb665467a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java @@ -6,8 +6,10 @@ */ package org.hibernate.loader.ast.internal; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; @@ -20,6 +22,7 @@ import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.spi.Limit; import org.hibernate.spi.NavigablePath; import org.hibernate.query.ResultListTransformer; @@ -35,6 +38,7 @@ import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; import org.hibernate.sql.ast.spi.SqlAstQueryPartProcessingState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -103,6 +107,21 @@ public class LoaderSqlAstCreationState return processingState.getInflightQueryPart(); } + @Override + public void registerTreat(TableGroup tableGroup, EntityDomainType treatType) { + throw new UnsupportedOperationException(); + } + + @Override + public void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType) { + throw new UnsupportedOperationException(); + } + + @Override + public Map, Boolean>> getTreatRegistrations() { + return Collections.emptyMap(); + } + @Override public SqlExpressionResolver getSqlExpressionResolver() { return processingState; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java index 058a42a6df..6e1543a399 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Set; import java.util.StringTokenizer; +import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.Remove; import org.hibernate.boot.Metadata; @@ -1331,4 +1332,45 @@ public abstract class PersistentClass implements AttributeContainer, Serializabl public List getCheckConstraints() { return checkConstraints; } + + private boolean containsColumn(Column column) { + for ( Property declaredProperty : declaredProperties ) { + if ( declaredProperty.getSelectables().contains( column ) ) { + return true; + } + } + return false; + } + + @Internal + public boolean isDefinedOnMultipleSubclasses(Column column) { + PersistentClass declaringType = null; + for ( PersistentClass persistentClass : getSubclassClosure() ) { + if ( persistentClass.containsColumn( column ) ) { + if ( declaringType != null && declaringType != persistentClass ) { + return true; + } + else { + declaringType = persistentClass; + } + } + } + return false; + } + + @Internal + public PersistentClass getSuperPersistentClass() { + return getSuperclass() != null ? getSuperclass() : getSuperPersistentClass( getSuperMappedSuperclass() ); + } + + private static PersistentClass getSuperPersistentClass(MappedSuperclass mappedSuperclass) { + if ( mappedSuperclass != null ) { + final PersistentClass superClass = mappedSuperclass.getSuperPersistentClass(); + if ( superClass != null ) { + return superClass; + } + return getSuperPersistentClass( mappedSuperclass.getSuperMappedSuperclass() ); + } + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index 7fc6edd437..db305ff9c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -27,6 +27,7 @@ import org.hibernate.mapping.Contributable; import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; @@ -245,6 +246,34 @@ public interface EntityMappingType return superMappingType.getRootEntityDescriptor(); } + /** + * Adapts the table group and its table reference as well as table reference joins + * in a way such that unnecessary tables or joins are omitted if possible, + * based on the given treated entity names. + *

+ * The goal is to e.g. remove join inheritance "branches" or union selects that are impossible. + *

+ * Consider the following example: + * + * class BaseEntity {} + * class Sub1 extends BaseEntity {} + * class Sub1Sub1 extends Sub1 {} + * class Sub1Sub2 extends Sub1 {} + * class Sub2 extends BaseEntity {} + * class Sub2Sub1 extends Sub2 {} + * class Sub2Sub2 extends Sub2 {} + * + *

+ * If the treatedEntityNames only contains Sub1 or any of its subtypes, + * this means that Sub2 and all subtypes are impossible, + * thus the joins/selects for these types shall be omitted in the given table group. + * + * @param tableGroup The table group to prune subclass tables for + * @param entityNameUses The entity names under which a table group was used. + */ + default void pruneForSubclasses(TableGroup tableGroup, Map entityNameUses) { + } + /** * Adapts the table group and its table reference as well as table reference joins * in a way such that unnecessary tables or joins are omitted if possible, @@ -269,7 +298,9 @@ public interface EntityMappingType * * @param tableGroup The table group to prune subclass tables for * @param treatedEntityNames The entity names for which path usages were registered + * @deprecated Use {@link #pruneForSubclasses(TableGroup, Map)} instead */ + @Deprecated(forRemoval = true) default void pruneForSubclasses(TableGroup tableGroup, Set treatedEntityNames) { } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractManagedType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractManagedType.java index 7486a5a0a0..85c5a4c0d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractManagedType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractManagedType.java @@ -9,6 +9,7 @@ package org.hibernate.metamodel.model.domain; import java.io.ObjectStreamException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -58,7 +59,7 @@ public abstract class AbstractManagedType private volatile Map> declaredPluralAttributes ; private volatile Map> declaredConcreteGenericAttributes; - private final List subTypes = new ArrayList<>(); + private final List> subTypes = new ArrayList<>(); protected AbstractManagedType( String hibernateTypeName, @@ -87,6 +88,11 @@ public abstract class AbstractManagedType return superType; } + @Override + public Collection> getSubTypes() { + return subTypes; + } + @Override public void addSubType(ManagedDomainType subType){ subTypes.add( subType ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/EntityDomainType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/EntityDomainType.java index 8247b3a709..f68925ba9a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/EntityDomainType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/EntityDomainType.java @@ -6,6 +6,8 @@ */ package org.hibernate.metamodel.model.domain; +import java.util.Collection; + import jakarta.persistence.metamodel.EntityType; import org.hibernate.query.sqm.SqmPathSource; @@ -17,4 +19,7 @@ import org.hibernate.query.sqm.SqmPathSource; */ public interface EntityDomainType extends IdentifiableDomainType, EntityType, SqmPathSource { String getHibernateEntityName(); + + @Override + Collection> getSubTypes(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/ManagedDomainType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/ManagedDomainType.java index a1d437d9a8..59cd63f631 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/ManagedDomainType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/ManagedDomainType.java @@ -6,6 +6,7 @@ */ package org.hibernate.metamodel.model.domain; +import java.util.Collection; import java.util.function.Consumer; import org.hibernate.graph.spi.SubGraphImplementor; @@ -41,6 +42,8 @@ public interface ManagedDomainType extends SqmExpressible, DomainType, */ ManagedDomainType getSuperType(); + Collection> getSubTypes(); + void addSubType(ManagedDomainType subType); void visitAttributes(Consumer> action); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java index 31178b55d5..d212d3a44b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java @@ -8,6 +8,8 @@ package org.hibernate.metamodel.model.domain.internal; import java.io.ObjectStreamException; import java.io.Serializable; +import java.util.Collection; + import jakarta.persistence.metamodel.EntityType; import org.hibernate.graph.internal.SubGraphImpl; @@ -21,6 +23,7 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; @@ -169,6 +172,12 @@ public class EntityTypeImpl return super.getSuperType(); } + @Override + public Collection> getSubTypes() { + //noinspection unchecked + return (Collection>) super.getSubTypes(); + } + @Override @SuppressWarnings("unchecked") public SubGraphImplementor makeSubGraph(Class subType) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java index 60ac07962a..bcabe4a4ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java @@ -85,6 +85,7 @@ import org.hibernate.persister.collection.mutation.CollectionMutationTarget; import org.hibernate.persister.collection.mutation.CollectionTableMapping; import org.hibernate.persister.collection.mutation.RemoveCoordinator; import org.hibernate.persister.collection.mutation.RowMutationOperations; +import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Joinable; import org.hibernate.persister.entity.PropertyMapping; @@ -533,7 +534,16 @@ public abstract class AbstractCollectionPersister filterHelper = null; } else { - filterHelper = new FilterHelper( collectionBootDescriptor.getFilters(), factory); + final Map entityNameByTableNameMap; + if ( elementPersister == null ) { + entityNameByTableNameMap = null; + } + else { + entityNameByTableNameMap = AbstractEntityPersister.getEntityNameByTableNameMap( + creationContext.getBootModel().getEntityBinding( elementPersister.getEntityName() ) + ); + } + filterHelper = new FilterHelper( collectionBootDescriptor.getFilters(), entityNameByTableNameMap, factory ); } // Handle any filters applied to this collectionBinding for many-to-many @@ -1186,12 +1196,19 @@ public abstract class AbstractCollectionPersister } @Override - public void applyFilterRestrictions(Consumer predicateConsumer, TableGroup tableGroup, boolean useQualifier, Map enabledFilters, SqlAstCreationState creationState) { + public void applyFilterRestrictions( + Consumer predicateConsumer, + TableGroup tableGroup, + boolean useQualifier, + Map enabledFilters, + SqlAstCreationState creationState) { if ( filterHelper != null ) { filterHelper.applyEnabledFilters( predicateConsumer, getFilterAliasGenerator( tableGroup ), - enabledFilters + enabledFilters, + tableGroup, + creationState ); } } @@ -1200,7 +1217,13 @@ public abstract class AbstractCollectionPersister public abstract boolean isManyToMany(); @Override - public void applyBaseManyToManyRestrictions(Consumer predicateConsumer, TableGroup tableGroup, boolean useQualifier, Map enabledFilters, Set treatAsDeclarations, SqlAstCreationState creationState) { + public void applyBaseManyToManyRestrictions( + Consumer predicateConsumer, + TableGroup tableGroup, + boolean useQualifier, + Map enabledFilters, + Set treatAsDeclarations, + SqlAstCreationState creationState) { if ( manyToManyFilterHelper == null && manyToManyWhereTemplate == null ) { return; } @@ -1208,7 +1231,13 @@ public abstract class AbstractCollectionPersister if ( manyToManyFilterHelper != null ) { final FilterAliasGenerator aliasGenerator = elementPersister.getFilterAliasGenerator( tableGroup ); - manyToManyFilterHelper.applyEnabledFilters( predicateConsumer, aliasGenerator, enabledFilters ); + manyToManyFilterHelper.applyEnabledFilters( + predicateConsumer, + aliasGenerator, + enabledFilters, + tableGroup, + creationState + ); } if ( manyToManyWhereString != null ) { 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 84eb58f5b3..8fce367906 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 @@ -110,6 +110,7 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.FilterAliasGenerator; import org.hibernate.internal.FilterHelper; +import org.hibernate.internal.FilterImpl; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.internal.util.LazyValue; import org.hibernate.internal.util.StringHelper; @@ -143,6 +144,7 @@ import org.hibernate.mapping.Column; import org.hibernate.mapping.Component; import org.hibernate.mapping.DependantValue; import org.hibernate.mapping.Formula; +import org.hibernate.mapping.Join; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.Selectable; @@ -358,6 +360,7 @@ public abstract class AbstractEntityPersister private final String[][] propertyColumnWriters; private final boolean[][] propertyColumnUpdateable; private final boolean[][] propertyColumnInsertable; + private final Set sharedColumnNames; private final List lobProperties; @@ -434,6 +437,7 @@ public abstract class AbstractEntityPersister protected ReflectionOptimizer.AccessOptimizer accessOptimizer; + protected final String[] fullDiscriminatorSQLValues; private final Object[] fullDiscriminatorValues; /** @@ -599,6 +603,7 @@ public abstract class AbstractEntityPersister propertyColumnWriters = new String[hydrateSpan][]; propertyColumnUpdateable = new boolean[hydrateSpan][]; propertyColumnInsertable = new boolean[hydrateSpan][]; + sharedColumnNames = new HashSet<>(); final HashSet thisClassProperties = new HashSet<>(); final ArrayList lazyNames = new ArrayList<>(); @@ -736,6 +741,10 @@ public abstract class AbstractEntityPersister typeConfiguration, functionRegistry ); + if ( isDefinedBySubclass && persistentClass.isDefinedOnMultipleSubclasses( column ) + || !isDefinedBySubclass && persistentClass.hasSubclasses() ) { + sharedColumnNames.add( colName ); + } } } propColumns.add( cols ); @@ -772,7 +781,7 @@ public abstract class AbstractEntityPersister // Handle any filters applied to the class level filterHelper = isNotEmpty( persistentClass.getFilters() ) - ? new FilterHelper( persistentClass.getFilters(), factory ) + ? new FilterHelper( persistentClass.getFilters(), getEntityNameByTableNameMap( persistentClass ), factory ) : null; useReferenceCacheEntries = shouldUseReferenceCacheEntries( creationContext.getSessionFactoryOptions() ); @@ -782,12 +791,12 @@ public abstract class AbstractEntityPersister && shouldInvalidateCache( persistentClass, creationContext ); final List values = new ArrayList<>(); -// final List sqlValues = new ArrayList<>(); + final List sqlValues = new ArrayList<>(); if ( persistentClass.isPolymorphic() && persistentClass.getDiscriminator() != null ) { if ( !getEntityMetamodel().isAbstract() ) { values.add( DiscriminatorHelper.getDiscriminatorValue( persistentClass ) ); -// sqlValues.add( DiscriminatorHelper.getDiscriminatorSQLValue( persistentClass, dialect, factory ) ); + sqlValues.add( DiscriminatorHelper.getDiscriminatorSQLValue( persistentClass, dialect ) ); } final List subclasses = persistentClass.getSubclasses(); @@ -796,15 +805,34 @@ public abstract class AbstractEntityPersister //copy/paste from EntityMetamodel: if ( !isAbstract( subclass ) ) { values.add( DiscriminatorHelper.getDiscriminatorValue( subclass ) ); -// sqlValues.add( DiscriminatorHelper.getDiscriminatorSQLValue( subclass, dialect, factory ) ); + sqlValues.add( DiscriminatorHelper.getDiscriminatorSQLValue( subclass, dialect ) ); } } } -// fullDiscriminatorSQLValues = toStringArray( sqlValues ); + fullDiscriminatorSQLValues = toStringArray( sqlValues ); fullDiscriminatorValues = toObjectArray( values ); } + public static Map getEntityNameByTableNameMap(PersistentClass persistentClass) { + final Map entityNameByTableNameMap = new HashMap<>(); + PersistentClass superType = persistentClass.getSuperPersistentClass(); + while ( superType != null ) { + entityNameByTableNameMap.put( superType.getTable().getName(), superType.getEntityName() ); + for ( Join join : superType.getJoins() ) { + entityNameByTableNameMap.put( join.getTable().getName(), superType.getEntityName() ); + } + superType = superType.getSuperPersistentClass(); + } + for ( PersistentClass subclass : persistentClass.getSubclassClosure() ) { + entityNameByTableNameMap.put( subclass.getTable().getName(), subclass.getEntityName() ); + for ( Join join : subclass.getJoins() ) { + entityNameByTableNameMap.put( join.getTable().getName(), subclass.getEntityName() ); + } + } + return entityNameByTableNameMap; + } + private MultiIdEntityLoader buildMultiIdLoader(PersistentClass persistentClass) { if ( persistentClass.getIdentifier() instanceof BasicValue && MultiKeyLoadHelper.supportsSqlArrayType( factory.getServiceRegistry().getService( JdbcServices.class ).getDialect() ) ) { @@ -945,6 +973,10 @@ public abstract class AbstractEntityPersister return entityMetamodel.getSubclassEntityNames().contains( entityName ); } + public boolean isSharedColumn(String columnExpression) { + return sharedColumnNames.contains( columnExpression ); + } + protected boolean[] getTableHasColumns() { return tableHasColumns; } @@ -3007,7 +3039,13 @@ public abstract class AbstractEntityPersister final FilterAliasGenerator filterAliasGenerator = useQualifier && tableGroup != null ? getFilterAliasGenerator( tableGroup ) : null; - filterHelper.applyEnabledFilters( predicateConsumer, filterAliasGenerator, enabledFilters ); + filterHelper.applyEnabledFilters( + predicateConsumer, + filterAliasGenerator, + enabledFilters, + tableGroup, + creationState + ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java new file mode 100644 index 0000000000..c4c07ba7b2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java @@ -0,0 +1,96 @@ +/* + * 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.persister.entity; + +import org.hibernate.Incubating; + +/** + * Describes the kind of entity name use. + */ +@Incubating +public final class EntityNameUse { + + public static final EntityNameUse PROJECTION = new EntityNameUse( UseKind.PROJECTION, true ); + public static final EntityNameUse EXPRESSION = new EntityNameUse( UseKind.EXPRESSION, true ); + public static final EntityNameUse TREAT = new EntityNameUse( UseKind.TREAT, true ); + public static final EntityNameUse OPTIONAL_TREAT = new EntityNameUse( UseKind.TREAT, false ); + public static final EntityNameUse FILTER = new EntityNameUse( UseKind.FILTER, true ); + + private final UseKind kind; + private final boolean requiresRestriction; + + private EntityNameUse(UseKind kind, boolean requiresRestriction) { + this.kind = kind; + this.requiresRestriction = requiresRestriction; + } + + private static EntityNameUse get(UseKind kind) { + switch ( kind ) { + case PROJECTION: + return PROJECTION; + case EXPRESSION: + return EXPRESSION; + case TREAT: + return TREAT; + case FILTER: + return FILTER; + } + throw new IllegalArgumentException( "Unknown kind: " + kind ); + } + + public UseKind getKind() { + return kind; + } + + public boolean requiresRestriction() { + return requiresRestriction; + } + + public EntityNameUse stronger(EntityNameUse other) { + return other == null || kind.isStrongerThan( other.kind ) ? this : get( other.kind ); + } + + public EntityNameUse weaker(EntityNameUse other) { + return other == null || kind.isWeakerThan( other.kind ) ? this : get( other.kind ); + } + + public enum UseKind { + /** + * An entity type is used through a path that appears in the {@code SELECT} clause somehow. + * This use kind is registered for top level select items or join fetches. + */ + PROJECTION, + /** + * An entity type is used through a path expression, but doesn't match the criteria for {@link #PROJECTION}. + */ + EXPRESSION, + /** + * An entity type is used through a treat expression. + */ + TREAT, + /** + * An entity type is filtered for through a type restriction predicate i.e. {@code type(alias) = Subtype}. + */ + FILTER; + + public boolean isStrongerThan(UseKind other) { + return ordinal() > other.ordinal(); + } + + public UseKind stronger(UseKind other) { + return other == null || isStrongerThan( other ) ? this : other; + } + + public boolean isWeakerThan(UseKind other) { + return ordinal() < other.ordinal(); + } + + public UseKind weaker(UseKind other) { + return other == null || isWeakerThan( other ) ? this : other; + } + } +} 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 e15cc8ac89..61907235ff 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 @@ -162,6 +162,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { // Span of the tables directly mapped by this entity and super-classes, if any private final int coreTableSpan; + private final int subclassCoreTableSpan; // only contains values for SecondaryTables, ie. not tables part of the "coreTableSpan" private final boolean[] isNullableTable; private final boolean[] isInverseTable; @@ -274,6 +275,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { //Span of the tableNames directly mapped by this entity and super-classes, if any coreTableSpan = tableNames.size(); + subclassCoreTableSpan = persistentClass.getSubclassTableClosure().size(); tableSpan = persistentClass.getJoinClosureSpan() + coreTableSpan; isNullableTable = new boolean[tableSpan]; @@ -1302,57 +1304,104 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { } @Override - public void pruneForSubclasses(TableGroup tableGroup, Set treatedEntityNames) { - final Set retainedTableReferences = new HashSet<>( treatedEntityNames.size() ); - final Set sharedSuperclassTables = new HashSet<>(); + public void pruneForSubclasses(TableGroup tableGroup, Map entityNameUses) { + final Set retainedTableReferences = new HashSet<>( entityNameUses.size() ); final MappingMetamodelImplementor metamodel = getFactory().getRuntimeMetamodels().getMappingMetamodel(); - - for ( String treatedEntityName : treatedEntityNames ) { - final JoinedSubclassEntityPersister persister = - (JoinedSubclassEntityPersister) metamodel.findEntityDescriptor( treatedEntityName ); - final String[] subclassTableNames = persister.getSubclassTableNames(); - // For every treated entity name, we collect table names that are needed by all treated entity names - // In mathematical terms, sharedSuperclassTables will be the "intersection" of the table names of all treated entities - if ( sharedSuperclassTables.isEmpty() ) { - for ( int i = 0; i < subclassTableNames.length; i++ ) { - if ( persister.isClassOrSuperclassTable[i] ) { - sharedSuperclassTables.add( subclassTableNames[i] ); - } - } - } - else { - sharedSuperclassTables.retainAll( Arrays.asList( subclassTableNames ) ); - } - // Add the table references for all table names of the treated entities as we have to retain these table references. - // Table references not appearing in this set can later be pruned away - for ( String subclassTableName : subclassTableNames ) { - final TableReference tableReference = - tableGroup.getTableReference( null, subclassTableName, false ); - if ( tableReference == null ) { - throw new UnknownTableReferenceException( getRootTableName(), "Couldn't find table reference" ); - } - retainedTableReferences.add( tableReference ); - } - } - final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); - // The optimization is to remove all table reference joins that are not contained in the retainedTableReferences - // In addition, we switch from a possible LEFT join, to an inner join for all sharedSuperclassTables - // For now, we can only do this if the table group reports canUseInnerJoins or isRealTableGroup, + // We can only do this optimization if the table group reports canUseInnerJoins or isRealTableGroup, // because the switch for table reference joins to INNER must be cardinality preserving. // If canUseInnerJoins is true, this is trivially given, but also if the table group is real // i.e. with parenthesis around, as that means the table reference joins will be isolated - if ( tableGroup.canUseInnerJoins() || tableGroup.isRealTableGroup() ) { + final boolean innerJoinOptimization = tableGroup.canUseInnerJoins() || tableGroup.isRealTableGroup(); + final Set tablesToInnerJoin = innerJoinOptimization ? new HashSet<>() : null; + for ( Map.Entry entry : entityNameUses.entrySet() ) { + final EntityNameUse.UseKind useKind = entry.getValue().getKind(); + final JoinedSubclassEntityPersister persister = + (JoinedSubclassEntityPersister) metamodel.findEntityDescriptor( entry.getKey() ); + // The following block tries to figure out what can be inner joined and which super class table joins can be omitted + if ( innerJoinOptimization && ( useKind == EntityNameUse.UseKind.TREAT || useKind == EntityNameUse.UseKind.FILTER ) ) { + final String[] subclassTableNames = persister.getSubclassTableNames(); + // Build the intersection of all tables names that are of the class or super class + // These are the tables that can be safely inner joined + if ( tablesToInnerJoin.isEmpty() ) { + for ( int i = 0; i < subclassTableNames.length; i++ ) { + if ( persister.isClassOrSuperclassTable[i] ) { + tablesToInnerJoin.add( subclassTableNames[i] ); + } + } + } + else { + tablesToInnerJoin.retainAll( Arrays.asList( subclassTableNames ) ); + } + if ( useKind == EntityNameUse.UseKind.FILTER && explicitDiscriminatorColumnName == null ) { + // If there is no discriminator column, + // we must retain all joins to subclass tables to be able to discriminate the rows + for ( int i = 0; i < subclassTableNames.length; i++ ) { + if ( !persister.isClassOrSuperclassTable[i] ) { + final String subclassTableName = subclassTableNames[i]; + final TableReference mainTableReference = tableGroup.getTableReference( + null, + subclassTableName, + false + ); + if ( mainTableReference == null ) { + throw new UnknownTableReferenceException( + subclassTableName, + "Couldn't find table reference" + ); + } + retainedTableReferences.add( mainTableReference ); + } + } + } + } + final TableReference mainTableReference = tableGroup.getTableReference( + null, + persister.getTableName(), + false + ); + if ( mainTableReference == null ) { + throw new UnknownTableReferenceException( persister.getTableName(), "Couldn't find table reference" ); + } + 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() ) { + final String[] subclassTableNames = getSubclassTableNames(); + for ( int i = 0; i < subclassTableNames.length; i++ ) { + if ( isClassOrSuperclassTable[i] ) { + tablesToInnerJoin.add( subclassTableNames[i] ); + } + } + } + + final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + if ( tableReferenceJoins.isEmpty() ) { + return; + } + // The optimization is to remove all table reference joins that are not contained in the retainedTableReferences + // In addition, we switch from a possible LEFT join, to an INNER join for all tablesToInnerJoin + if ( innerJoinOptimization ) { final TableReferenceJoin[] oldJoins = tableReferenceJoins.toArray( new TableReferenceJoin[0] ); tableReferenceJoins.clear(); for ( TableReferenceJoin oldJoin : oldJoins ) { final NamedTableReference joinedTableReference = oldJoin.getJoinedTableReference(); if ( retainedTableReferences.contains( joinedTableReference ) ) { final TableReferenceJoin join = oldJoin.getJoinType() != SqlAstJoinType.INNER - && sharedSuperclassTables.contains( joinedTableReference.getTableExpression() ) - ? new TableReferenceJoin(true, joinedTableReference, oldJoin.getPredicate()) + && tablesToInnerJoin.contains( joinedTableReference.getTableExpression() ) + ? new TableReferenceJoin( true, joinedTableReference, oldJoin.getPredicate() ) : oldJoin; tableReferenceJoins.add( join ); } + else { + final String tableExpression = oldJoin.getJoinedTableReference().getTableExpression(); + for ( int i = subclassCoreTableSpan; i < subclassTableNameClosure.length; i++ ) { + if ( tableExpression.equals( subclassTableNameClosure[i] ) ) { + // Retain joins to secondary tables + tableReferenceJoins.add( oldJoin ); + break; + } + } + } } } else { 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 ad9513c2de..3cbd17b1c2 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,10 +8,10 @@ package org.hibernate.persister.entity; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.Internal; @@ -23,6 +23,7 @@ import org.hibernate.dialect.Dialect; 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; @@ -34,13 +35,13 @@ import org.hibernate.mapping.Selectable; import org.hibernate.mapping.Subclass; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; -import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; 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; @@ -660,12 +661,36 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { } @Override - public void pruneForSubclasses(TableGroup tableGroup, Set treatedEntityNames) { - if ( !needsDiscriminator() && treatedEntityNames.isEmpty() ) { + public void pruneForSubclasses(TableGroup tableGroup, Map entityNameUses) { + if ( !needsDiscriminator() && entityNameUses.isEmpty() ) { + return; + } + // The following optimization is to add the discriminator filter fragment for all treated entity names + final MappingMetamodelImplementor mappingMetamodel = getFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + + boolean containsTreatUse = 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() && !isTypeOrSuperType( persister ) && useKind == EntityNameUse.UseKind.TREAT ) { + containsTreatUse = true; + break; + } + } + if ( !containsTreatUse ) { + // If we only have FILTER uses, we don't have to do anything here, + // because the BaseSqmToSqlAstConverter will already apply the type filter in the WHERE clause return; } - // The optimization is to simply add the discriminator filter fragment for all treated entity names - final NamedTableReference tableReference = (NamedTableReference) tableGroup.getPrimaryTableReference(); final InFragment frag = new InFragment(); if ( isDiscriminatorFormula() ) { @@ -674,34 +699,55 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { else { frag.setColumn( "t", getDiscriminatorColumnName() ); } - - final MappingMetamodelImplementor mappingMetamodel = getFactory() - .getRuntimeMetamodels() - .getMappingMetamodel(); - for ( String subclass : treatedEntityNames ) { - final EntityMappingType treatTargetType = mappingMetamodel.getEntityDescriptor( subclass ); - if ( !treatTargetType.isAbstract() ) { - frag.addValue( treatTargetType.getDiscriminatorSQLValue() ); + 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; } - if ( treatTargetType.hasSubclasses() ) { - // if the treat is an abstract class, add the concrete implementations to values if any - final Set actualSubClasses = treatTargetType.getSubclassEntityNames(); - for ( String actualSubClass : actualSubClasses ) { - if ( actualSubClass.equals( subclass ) ) { - continue; - } - - final EntityMappingType actualEntityDescriptor = mappingMetamodel.getEntityDescriptor( actualSubClass ); - if ( !actualEntityDescriptor.hasSubclasses() ) { - frag.addValue( actualEntityDescriptor.getDiscriminatorSQLValue() ); - } - } + 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; + } - tableReference.setPrunedTableExpression( - "(select * from " + getTableName() + " t where " + frag.toFragmentString() + ")" - ); + final NamedTableReference tableReference = (NamedTableReference) tableGroup.getPrimaryTableReference(); + if ( containsNotNull ) { + StringBuilder sb = new StringBuilder(); + String lhs; + if ( isDiscriminatorFormula() ) { + lhs = StringHelper.replace( getDiscriminatorFormulaTemplate(), Template.TEMPLATE, "t" ); + } + else { + lhs = "t." + getDiscriminatorColumnName(); + } + sb.append( " or " ).append( lhs ).append( " is not in (" ); + for ( Object discriminatorSQLValue : discriminatorSQLValues ) { + if ( !frag.getValues().contains( discriminatorSQLValue ) ) { + sb.append( lhs ).append( discriminatorSQLValue ); + } + } + sb.append( ") and " ).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() + ")" + ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java index e4e4623bb9..c72ad0f068 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java @@ -9,6 +9,7 @@ package org.hibernate.persister.entity; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -53,8 +54,6 @@ import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; -import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; -import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.UnionTableGroup; @@ -421,13 +420,13 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { } @Override - public void pruneForSubclasses(TableGroup tableGroup, Set treatedEntityNames) { + public void pruneForSubclasses(TableGroup tableGroup, Map entityNameUses) { final NamedTableReference tableReference = (NamedTableReference) tableGroup.getTableReference( getRootTableName() ); if ( tableReference == null ) { throw new UnknownTableReferenceException( getRootTableName(), "Couldn't find table reference" ); } // Replace the default union sub-query with a specially created one that only selects the tables for the treated entity names - tableReference.setPrunedTableExpression( generateSubquery( treatedEntityNames ) ); + tableReference.setPrunedTableExpression( generateSubquery( entityNameUses ) ); } @Override @@ -493,9 +492,9 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { } final StringBuilder subquery = new StringBuilder() - .append( "( " ); + .append( "(" ); - List classes = new JoinedList<>( + final List classes = new JoinedList<>( List.of( model ), Collections.unmodifiableList( model.getSubclasses() ) ); @@ -504,7 +503,7 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { Table table = clazz.getTable(); if ( !table.isAbstractUnionTable() ) { //TODO: move to .sql package!! - if ( subquery.length() > 2 ) { + if ( subquery.length() > 1 ) { subquery.append( " union " ); if ( dialect.supportsUnionAll() ) { subquery.append( "all " ); @@ -526,41 +525,64 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { } } - return subquery.append( " )" ).toString(); + return subquery.append( ")" ).toString(); } - protected String generateSubquery(Set treated) { + protected String generateSubquery(Map entityNameUses) { if ( !hasSubclasses() ) { return getTableName(); } final Dialect dialect = getFactory().getJdbcServices().getDialect(); final MappingMetamodelImplementor metamodel = getFactory().getRuntimeMetamodels().getMappingMetamodel(); - // Collect all selectables of every entity subtype and group by selection expression as well as table name final LinkedHashMap> selectables = new LinkedHashMap<>(); - // Collect the concrete subclass table names for the treated entity names - final Set treatedTableNames = new HashSet<>( treated.size() ); - for ( String subclassName : treated ) { - final UnionSubclassEntityPersister subPersister = - (UnionSubclassEntityPersister) metamodel.getEntityDescriptor( subclassName ); - // Collect all the real (non-abstract) table names - treatedTableNames.addAll( Arrays.asList( subPersister.getConstraintOrderedTableNameClosure() ) ); + final Set tablesToUnion = new HashSet<>( entityNameUses.size() ); + // Check if there are filter uses and if so, we know the set of tables to union already + for ( Map.Entry entry : entityNameUses.entrySet() ) { + final UnionSubclassEntityPersister persister = + (UnionSubclassEntityPersister) metamodel.getEntityDescriptor( entry.getKey() ); + if ( entry.getValue().getKind() == EntityNameUse.UseKind.FILTER && !persister.isAbstract() ) { + tablesToUnion.add( persister.getRootTableName() ); + } // Collect selectables grouped by the table names in which they appear - // TODO: we could cache this - subPersister.collectSelectableOwners( selectables ); + persister.collectSelectableOwners( selectables ); + } + + if ( tablesToUnion.isEmpty() ) { + // If there are no filter uses, we try to find the most specific treat uses and union all their subclass tables + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue().getKind() == EntityNameUse.UseKind.TREAT ) { + // Collect all the real (non-abstract) table names + final UnionSubclassEntityPersister persister = + (UnionSubclassEntityPersister) metamodel.getEntityDescriptor( entry.getKey() ); + tablesToUnion.addAll( Arrays.asList( persister.getConstraintOrderedTableNameClosure() ) ); + } + } + if ( tablesToUnion.isEmpty() ) { + // If there are only projection or expression uses, we can't optimize anything + return getTableName(); + } } // Create a union sub-query for the table names, like generateSubquery(PersistentClass model, Mapping mapping) - final StringBuilder buf = new StringBuilder( subquery.length() ) - .append( "( " ); - final StringBuilderSqlAppender sqlAppender = new StringBuilderSqlAppender( buf ); + final StringBuilder buf = new StringBuilder( subquery.length() ).append( "(" ); - for ( EntityMappingType mappingType : getSubMappingTypes() ) { + final Collection subMappingTypes = getSubMappingTypes(); + final ArrayList subMappingTypesAndThis = new ArrayList<>( subMappingTypes.size() + 1 ); + subMappingTypesAndThis.add( this ); + subMappingTypesAndThis.addAll( subMappingTypes ); + for ( EntityMappingType mappingType : subMappingTypesAndThis ) { final AbstractEntityPersister persister = (AbstractEntityPersister) mappingType; - final String subclassTableName = persister.getTableName(); - if ( treatedTableNames.contains( subclassTableName ) ) { - if ( buf.length() > 2 ) { + final String subclassTableName; + if ( persister.hasSubclasses() ) { + subclassTableName = persister.getRootTableName(); + } + else { + subclassTableName = persister.getTableName(); + } + if ( tablesToUnion.contains( subclassTableName ) ) { + if ( buf.length() > 1 ) { buf.append(" union "); if ( dialect.supportsUnionAll() ) { buf.append("all "); @@ -577,7 +599,12 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { buf.append( dialect.getSelectClauseNullString( sqlType, getFactory().getTypeConfiguration() ) ) .append( " as " ); } - new ColumnReference( (String) null, selectableMapping ).appendReadExpression( sqlAppender ); + if ( selectableMapping.isFormula() ) { + buf.append( selectableMapping.getSelectableName() ); + } + else { + buf.append( selectableMapping.getSelectionExpression() ); + } buf.append( ", " ); } buf.append( persister.getDiscriminatorSQLValue() ) @@ -585,7 +612,7 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { .append( subclassTableName ); } } - return buf.append( " )" ).toString(); + return buf.append( ")" ).toString(); } private void collectSelectableOwners(LinkedHashMap> selectables) { @@ -602,7 +629,14 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { selectable.getSelectionExpression(), k -> new HashMap<>() ); - selectableMapping.put( getTableName(), selectable ); + final String subclassTableName; + if ( hasSubclasses() ) { + subclassTableName = getRootTableName(); + } + else { + subclassTableName = getTableName(); + } + selectableMapping.put( subclassTableName, selectable ); }; getIdentifierMapping().forEachSelectable( selectableConsumer ); if ( getVersionMapping() != null ) { @@ -613,6 +647,11 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { for ( int i = 0; i < size; i++ ) { attributeMappings.get( i ).forEachSelectable( selectableConsumer ); } + for ( EntityMappingType subMappingType : getSubMappingTypes() ) { + if ( !subMappingType.isAbstract() ) { + ( (UnionSubclassEntityPersister) subMappingType ).collectSelectableOwners( selectables ); + } + } } } 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 238450d18b..a5d8797c16 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 @@ -53,7 +53,8 @@ public class SqmCreationHelper { ); } NavigablePath navigablePath = lhs.getNavigablePath(); - if ( lhs.getReferencedPathSource() instanceof PluralPersistentAttribute ) { + if ( lhs.getReferencedPathSource() instanceof PluralPersistentAttribute + && CollectionPart.Nature.fromName( subNavigable ) == null ) { navigablePath = navigablePath.append( CollectionPart.Nature.ELEMENT.getName() ); } return buildSubNavigablePath( navigablePath, subNavigable, alias ); 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 1065703d8a..9c1daf8cc8 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 @@ -93,8 +93,10 @@ import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPathSource; @@ -104,6 +106,7 @@ import org.hibernate.metamodel.model.domain.internal.EntityDiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl; import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.SingleTableEntityPersister; import org.hibernate.query.BindableType; @@ -477,16 +480,13 @@ public abstract class BaseSqmToSqlAstConverter extends Base private final Stack processingStateStack = new StandardStack<>( SqlAstProcessingState.class ); private final Stack fromClauseIndexStack = new StandardStack<>( FromClauseIndex.class ); /* - * Captures all entity names as which a table group was treated. - * This information is used to prune tables from the table group. + * Captures all entity name uses under which a table group is being used within the current conjunct. + * Outside of a top level conjunct, it represents the "global uses" i.e. select, from, group and order by clauses. + * Top level conjunct contexts like visitWhereClause, visitHavingClause, visitOrPredicate and visitNestedTopLevelPredicate + * stash away the parent entity name uses, consumes the entity name uses of the conjunct by rendering a type restriction, + * and then restore the parent entity name uses again. */ - private final Map> tableGroupTreatUsages = new IdentityHashMap<>(); - /* - * Used to capture the treat usages within the current conjunct. - * The top level conjunct contexts like visitWhereClause, visitHavingClause, visitOrPredicate and - * visitNestedTopLevelPredicate consume these treat usages by rendering a type restriction - */ - private final Map, Set> conjunctTreatUsages = new IdentityHashMap<>(); + private final Map> tableGroupEntityNameUses = new IdentityHashMap<>(); private SqlAstProcessingState lastPoppedProcessingState; private FromClauseIndex lastPoppedFromClauseIndex; private SqmJoin currentlyProcessingJoin; @@ -1991,7 +1991,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base && ( sqmQuerySpec.getOrderByClause() != null || !sqmQuerySpec.getGroupByClauseExpressions().isEmpty() ); } - final SqlAstProcessingState processingState; + final SqlAstQueryPartProcessingStateImpl processingState; if ( trackAliasedNodePositions ) { processingState = new SqlAstQueryPartProcessingStateImpl( sqlQuerySpec, @@ -2055,6 +2055,27 @@ public abstract class BaseSqmToSqlAstConverter extends Base applyCollectionFilterPredicates( sqlQuerySpec ); } + // Look for treat registrations that have never been used in the query. + // These treats become "global" i.e. we need to apply filtering for all subtypes. + // This tracking is necessary to differentiate between the HQLs + // - from Root r join treat(r.attribute as Subtype) a where a.id = 1 or 1=1 + // - from Root r join r.attribute a where treat(a as Subtype).id = 1 or 1=1 + for ( Map.Entry, Boolean>> entry : processingState.getTreatRegistrations().entrySet() ) { + final TableGroup actualTableGroup = entry.getKey(); + final Map, Boolean> treatUses = entry.getValue(); + for ( Map.Entry, Boolean> treatUseEntry : treatUses.entrySet() ) { + final EntityDomainType treatTargetType = treatUseEntry.getKey(); + if ( !treatUseEntry.getValue() ) { + // The treat registration was not used in the query + registerEntityNameUsage( + actualTableGroup, + EntityNameUse.TREAT, + treatTargetType.getHibernateEntityName() + ); + } + } + } + QuerySpec finalQuerySpec = sqlQuerySpec; for ( QueryTransformer transformer : (List) queryTransformers.getCurrent() ) { finalQuerySpec = transformer.transform( @@ -2824,7 +2845,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } assert ownerTableGroup != null; - final TableGroup actualTableGroup = findActualTableGroup( ownerTableGroup, join ); + final TableGroup actualTableGroup = getActualTableGroup( ownerTableGroup, join ); lastTableGroup = consumeExplicitJoin( join, lastTableGroup, actualTableGroup, false ); } } @@ -2837,43 +2858,185 @@ public abstract class BaseSqmToSqlAstConverter extends Base .getEntityDescriptor( entityDomainType.getHibernateEntityName() ); } - protected void registerTreatUsage(SqmFrom sqmFrom, TableGroup tableGroup) { + /** + * Registers {@link EntityNameUse#PROJECTION} entity name uses for all entity valued path subtypes. + * If the path is a treat, registers {@link EntityNameUse#TREAT} for all treated subtypes instead. + */ + private void registerEntityNameProjectionUsage(SqmPath projectedPath, TableGroup tableGroup) { final EntityDomainType treatedType; - if ( sqmFrom instanceof SqmTreatedPath ) { - treatedType = ( (SqmTreatedPath) sqmFrom ).getTreatTarget(); + if ( projectedPath instanceof SqmTreatedPath ) { + treatedType = ( (SqmTreatedPath) projectedPath ).getTreatTarget(); + registerEntityNameUsage( tableGroup, EntityNameUse.TREAT, treatedType.getHibernateEntityName() ); + + // Register that this treat was used somewhere + ((SqlAstQueryPartProcessingState) getCurrentProcessingState()).registerTreatUsage( tableGroup, treatedType ); } - else { + else if ( projectedPath.getNodeType().getSqmPathType() instanceof EntityDomainType ) { + treatedType = (EntityDomainType) projectedPath.getNodeType().getSqmPathType(); + registerEntityNameUsage( tableGroup, EntityNameUse.PROJECTION, treatedType.getHibernateEntityName() ); + } + } + + /** + * If the {@link SqmPath} has a {@link PersistentAttribute} as {@link SqmPathSource}, + * this method determines the declaring entity type of the attribute and register a {@link EntityNameUse#EXPRESSION} + * for the given table group. If the parent path is a treat e.g. {@code treat(alias as Subtype).attribute}, + * it will instead register a {@link EntityNameUse#TREAT} for the treated type. + */ + private void registerPathAttributeEntityNameUsage(SqmPath sqmPath, TableGroup tableGroup) { + final SqmPathSource resolvedModel; + if ( !( sqmPath instanceof SqmTreatedPath ) + && tableGroup.getModelPart().getPartMappingType() instanceof EntityMappingType + && ( resolvedModel = sqmPath.getResolvedModel() ) instanceof PersistentAttribute ) { + final SqmPath parentPath = sqmPath.getLhs(); + final String attributeName = resolvedModel.getPathName(); + final EntityMappingType entityType = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); + final EntityMappingType parentType; + final EntityNameUse entityNameUse; + final String treatedEntityName; + if ( parentPath instanceof SqmTreatedPath ) { + // A treated attribute usage i.e. `treat(alias as Subtype).attribute = 1` + + final EntityDomainType treatTarget = ( (SqmTreatedPath) parentPath ).getTreatTarget(); + final AbstractEntityPersister persister = (AbstractEntityPersister) creationContext.getMappingMetamodel() + .getEntityDescriptor( treatTarget.getHibernateEntityName() ); + ( (SqlAstQueryPartProcessingState) getCurrentProcessingState() ).registerTreatUsage( + tableGroup, + treatTarget + ); + + treatedEntityName = treatTarget.getHibernateEntityName(); + parentType = persister; + + // The following is an optimization to avoid rendering treat conditions into predicates. + // Imagine an HQL predicate like `treat(alias as Subtype).attribute is null or alias.name = '...'`. + // If the column for `attribute` is not "shared", meaning that the column is valid only for one subtype, + // then we can safely skip adding the `type(alias) = Subtype and ...` condition to the SQL. + + final ModelPart subPart = parentType.findSubPart( attributeName ); + // We only apply this optimization for basic valued model parts for now + if ( subPart instanceof BasicValuedModelPart + && !persister.isSharedColumn( ( (BasicValuedModelPart) subPart ).getSelectionExpression() ) ) { + entityNameUse = EntityNameUse.OPTIONAL_TREAT; + } + else { + entityNameUse = EntityNameUse.TREAT; + } + } + else { + // A simple attribute usage e.g. `alias.attribute = 1` + treatedEntityName = null; + parentType = entityType; + entityNameUse = EntityNameUse.EXPRESSION; + } + final AttributeMapping attributeMapping = parentType.findAttributeMapping( attributeName ); + if ( attributeMapping == null ) { + if ( attributeName.equals( parentType.getIdentifierMapping().getPartName() ) ) { + // Until HHH-16571 is fixed, we must register an entity name use for the root entity descriptor name + registerEntityNameUsage( + tableGroup, + entityNameUse, + parentType.getRootEntityDescriptor().getEntityName() + ); + } + else { + // If the attribute mapping can't be found on the declaring type and it is not the identifier, + // this signals that we are working with an arbitrarily chosen attribute from a subclass. + // Register entity name usages for all subtypes that declare the attribute with the same name then + for ( EntityMappingType subMappingType : parentType.getSubMappingTypes() ) { + if ( subMappingType.findDeclaredAttributeMapping( attributeName ) != null ) { + registerEntityNameUsage( tableGroup, entityNameUse, subMappingType.getEntityName() ); + } + } + } + } + else { + registerEntityNameUsage( + tableGroup, + entityNameUse, + treatedEntityName == null + ? attributeMapping.findContainingEntityMapping().getEntityName() + : treatedEntityName + ); + } + } + } + + @Override + public void registerEntityNameUsage( + TableGroup tableGroup, + EntityNameUse entityNameUse, + String hibernateEntityName) { + final AbstractEntityPersister persister = (AbstractEntityPersister) creationContext.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .findEntityDescriptor( hibernateEntityName ); + if ( persister == null || !persister.isPolymorphic() ) { return; } final TableGroup actualTableGroup; if ( tableGroup instanceof PluralTableGroup ) { actualTableGroup = ( (PluralTableGroup) tableGroup ).getElementTableGroup(); } + else if ( tableGroup instanceof CorrelatedTableGroup ) { + actualTableGroup = ( (CorrelatedTableGroup) tableGroup ).getCorrelatedTableGroup(); + } else { actualTableGroup = tableGroup; } - final Set treatedEntityNames = tableGroupTreatUsages.computeIfAbsent( + final Map entityNameUses = tableGroupEntityNameUses.computeIfAbsent( actualTableGroup, - tg -> new HashSet<>( 1 ) + tg -> new HashMap<>( 1 ) + ); + entityNameUses.compute( + hibernateEntityName, + (s, existingUse) -> entityNameUse.stronger( existingUse ) ); - treatedEntityNames.add( treatedType.getHibernateEntityName() ); - // Resolve the table references for the tables that the treated type touches - final AbstractEntityPersister persister = (AbstractEntityPersister) creationContext.getSessionFactory() - .getRuntimeMetamodels() - .getEntityMappingType( treatedType.getHibernateEntityName() ); - // Avoid doing this for single table entity persisters, as the table span includes secondary tables, - // which we don't want to resolve, though we know that there is only a single table anyway - if ( persister instanceof SingleTableEntityPersister ) { - return; + // Resolve the table reference for all types which we register an entity name use for + actualTableGroup.resolveTableReference( null, persister.getTableName() ); + + if ( entityNameUse == EntityNameUse.PROJECTION ) { + // For projections also register uses of all super and subtypes, + // as well as resolve the respective table references + EntityMappingType superMappingType = persister; + while ( ( superMappingType = superMappingType.getSuperMappingType() ) != null ) { + entityNameUses.putIfAbsent( superMappingType.getEntityName(), EntityNameUse.PROJECTION ); + actualTableGroup.resolveTableReference( + null, + ( (AbstractEntityPersister) superMappingType.getEntityPersister() ).getTableName() + ); + } + + for ( String subclassEntityName : persister.getSubclassEntityNames() ) { + entityNameUses.putIfAbsent( subclassEntityName, EntityNameUse.PROJECTION ); + } + final int subclassTableSpan = persister.getSubclassTableSpan(); + for ( int i = 0; i < subclassTableSpan; i++ ) { + actualTableGroup.resolveTableReference( null, persister.getSubclassTableName( i ) ); + } } - final int subclassTableSpan = persister.getSubclassTableSpan(); - for ( int i = 0; i < subclassTableSpan; i++ ) { - tableGroup.resolveTableReference( null, persister.getSubclassTableName( i ) ); + else if ( entityNameUse == EntityNameUse.TREAT ) { + // 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 + for ( EntityMappingType subType : persister.getSubMappingTypes() ) { + entityNameUses.compute( + subType.getEntityName(), + (s, existingUse) -> entityNameUse.stronger( existingUse ) + ); + actualTableGroup.resolveTableReference( + null, + subType.getEntityPersister().getMappedTableDetails().getTableName() + ); + } } } protected void registerTypeUsage(EntityDiscriminatorSqmPath path) { + registerTypeUsage( getFromClauseAccess().getTableGroup( path.getNavigablePath().getParent() ) ); + } + + protected void registerTypeUsage(TableGroup tableGroup) { // When we encounter a discriminator path i.e. a use of `type( alias )` // we have to resolve all subclass tables, otherwise we might get wrong results // It might be worth deferring this process to the pruning phase when we start to prune subclass joins in more cases @@ -2885,7 +3048,6 @@ public abstract class BaseSqmToSqlAstConverter extends Base // but for `a = 1 and type(..) = A or type(..) = B` we can infer `A, B` // The OR junction allows to create a union of entity name lists of all sub-predicates // The AND junction allows to create an intersection of entity name lists of all sub-predicates - final TableGroup tableGroup = getFromClauseAccess().getTableGroup( path.getNavigablePath().getParent() ); final EntityMappingType mappingType = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); final AbstractEntityPersister persister = (AbstractEntityPersister) mappingType.getEntityPersister(); // Avoid doing this for single table entity persisters, as the table span includes secondary tables, @@ -2900,20 +3062,22 @@ public abstract class BaseSqmToSqlAstConverter extends Base } protected void pruneTableGroupJoins() { - for ( Map.Entry> entry : tableGroupTreatUsages.entrySet() ) { + for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { final TableGroup tableGroup = entry.getKey(); - final Set treatedEntityNames = entry.getValue(); - final ModelPartContainer modelPart = tableGroup.getModelPart(); - final EntityPersister tableGroupPersister; - if ( modelPart instanceof PluralAttributeMapping ) { - tableGroupPersister = (EntityPersister) ( (PluralAttributeMapping) modelPart ) - .getElementDescriptor() - .getPartMappingType(); + if ( tableGroup.isInitialized() ) { + final Map entityNameUses = entry.getValue(); + final ModelPartContainer modelPart = tableGroup.getModelPart(); + final EntityPersister tableGroupPersister; + if ( modelPart instanceof PluralAttributeMapping ) { + tableGroupPersister = (EntityPersister) ( (PluralAttributeMapping) modelPart ) + .getElementDescriptor() + .getPartMappingType(); + } + else { + tableGroupPersister = (EntityPersister) modelPart.getPartMappingType(); + } + tableGroupPersister.pruneForSubclasses( tableGroup, entityNameUses ); } - else { - tableGroupPersister = (EntityPersister) modelPart.getPartMappingType(); - } - tableGroupPersister.pruneForSubclasses( tableGroup, treatedEntityNames ); } } @@ -2923,15 +3087,20 @@ public abstract class BaseSqmToSqlAstConverter extends Base } sqmFrom.visitSqmJoins( sqmJoin -> { - final TableGroup actualTableGroup = findActualTableGroup( lhsTableGroup, sqmJoin ); - registerTreatUsage( (SqmFrom) sqmJoin.getLhs(), actualTableGroup ); + final TableGroup actualTableGroup = getActualTableGroup( lhsTableGroup, sqmJoin ); + registerPathAttributeEntityNameUsage( sqmJoin, actualTableGroup ); consumeExplicitJoin( sqmJoin, actualTableGroup, actualTableGroup, true ); } ); - for ( SqmFrom sqmTreat : sqmFrom.getSqmTreats() ) { - final TableGroup actualTableGroup = findActualTableGroup( lhsTableGroup, sqmTreat ); - registerTreatUsage( sqmTreat, actualTableGroup ); - consumeExplicitJoins( sqmTreat, actualTableGroup ); + final List> sqmTreats = sqmFrom.getSqmTreats(); + if ( !sqmTreats.isEmpty() ) { + final SqlAstQueryPartProcessingState queryPartProcessingState = (SqlAstQueryPartProcessingState) getCurrentProcessingState(); + for ( SqmFrom sqmTreat : sqmTreats ) { + final TableGroup actualTableGroup = getActualTableGroup( lhsTableGroup, sqmTreat ); + // We don't know the context yet in which a treat is used, so we have to register them first and track the usage + queryPartProcessingState.registerTreat( actualTableGroup, ( (SqmTreatedPath) sqmTreat ).getTreatTarget() ); + consumeExplicitJoins( sqmTreat, actualTableGroup ); + } } } @@ -2963,24 +3132,21 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } - private TableGroup findActualTableGroup(TableGroup lhsTableGroup, SqmPath path) { - final SqmPathSource intermediatePathSource; - final SqmPath lhs; - if ( path instanceof SqmTreatedPath ) { - lhs = ( (SqmTreatedPath) path ).getWrappedPath().getLhs(); + private TableGroup getActualTableGroup(TableGroup lhsTableGroup, SqmPath path) { + // The actual table group in case of PluralTableGroups usually is the element table group, + // but if the SqmPath is a SqmPluralPartJoin e.g. `join key(mapAlias) k` + // or the SqmPath is a simple path for the key e.g. `select key(mapAlias)`, + // then we want to return the PluralTableGroup instead + if ( lhsTableGroup instanceof PluralTableGroup + && !( path instanceof SqmPluralPartJoin ) + && CollectionPart.Nature.fromName( path.getNavigablePath().getLocalName() ) == null ) { + final TableGroup elementTableGroup = ( (PluralTableGroup) lhsTableGroup ).getElementTableGroup(); + // The element table group could be null for basic collections + if ( elementTableGroup != null ) { + return elementTableGroup; + } } - else { - lhs = path.getLhs(); - } - intermediatePathSource = lhs == null ? null : lhs.getReferencedPathSource() - .getIntermediatePathSource( path.getReferencedPathSource() ); - if ( intermediatePathSource == null ) { - return lhsTableGroup; - } - // The only possible intermediate path source for now is the element path source for plural attributes - assert intermediatePathSource.getPathName().equals( CollectionPart.Nature.ELEMENT.getName() ); - final PluralTableGroup pluralTableGroup = (PluralTableGroup) lhsTableGroup; - return pluralTableGroup.getElementTableGroup(); + return lhsTableGroup; } private TableGroup consumeAttributeJoin( @@ -3023,15 +3189,6 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); joinedTableGroup = joinedTableGroupJoin.getJoinedGroup(); -// -// pluralAttributeMapping.applyBaseRestrictions( -// (predicate) -> addCollectionFilterPredicate( joinedTableGroup.getNavigablePath(), predicate ), -// joinedTableGroup, -// true, -// getLoadQueryInfluencers().getEnabledFilters(), -// null, -// this -// ); } else { assert modelPart instanceof TableGroupJoinProducer; @@ -3087,6 +3244,11 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); } } + if ( sqmJoin.isFetched() ) { + // A fetch is like a projection usage, so register that properly + registerEntityNameProjectionUsage( sqmJoin, getActualTableGroup( joinedTableGroup, sqmJoin ) ); + } + registerPathAttributeEntityNameUsage( sqmJoin, ownerTableGroup ); // add any additional join restrictions if ( sqmJoin.getJoinPredicate() != null ) { @@ -3306,83 +3468,89 @@ public abstract class BaseSqmToSqlAstConverter extends Base final TableGroup existingTableGroup = fromClauseIndex.findTableGroupForGetOrCreate( sqmPath.getNavigablePath() ); if ( existingTableGroup == null ) { final TableGroup createdTableGroup = createTableGroup( - fromClauseIndex.getTableGroup( sqmPath.getLhs().getNavigablePath() ), + getActualTableGroup( + fromClauseIndex.getTableGroup( sqmPath.getLhs().getNavigablePath() ), + sqmPath + ), sqmPath ); if ( createdTableGroup != null ) { if ( sqmPath instanceof SqmTreatedPath ) { fromClauseIndex.register( sqmPath, createdTableGroup ); } - if ( sqmPath instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) sqmPath, createdTableGroup ); - } } } - else if ( sqmPath instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) sqmPath, existingTableGroup ); - } } return supplier.get(); } private TableGroup prepareReusablePath( FromClauseIndex fromClauseIndex, - JpaPath sqmPath, + JpaPath path, Consumer implicitJoinChecker) { - final JpaPath parentPath; + final SqmPath sqmPath = (SqmPath) path; + final SqmPath parentPath; if ( sqmPath instanceof SqmTreatedPath ) { parentPath = ( (SqmTreatedPath) sqmPath ).getWrappedPath(); } else { - parentPath = sqmPath.getParentPath(); + parentPath = sqmPath.getLhs(); } if ( parentPath == null ) { return null; } - final TableGroup tableGroup = fromClauseIndex.findTableGroupForGetOrCreate( parentPath.getNavigablePath() ); - if ( tableGroup == null ) { - final TableGroup parentTableGroup = prepareReusablePath( + final TableGroup parentTableGroup = getActualTableGroup( + fromClauseIndex.findTableGroupForGetOrCreate( parentPath.getNavigablePath() ), + sqmPath + ); + if ( parentTableGroup == null ) { + final TableGroup createdParentTableGroup = prepareReusablePath( fromClauseIndex, parentPath, implicitJoinChecker ); - if ( parentTableGroup == null ) { + if ( createdParentTableGroup == null ) { throw new SqlTreeCreationException( "Could not locate TableGroup - " + parentPath.getNavigablePath() ); } + final TableGroup newTableGroup; if ( parentPath instanceof SqmTreatedPath ) { - fromClauseIndex.register( (SqmPath) parentPath, parentTableGroup ); - return parentTableGroup; + fromClauseIndex.register( parentPath, createdParentTableGroup ); + newTableGroup = createdParentTableGroup; + } + else if ( createdParentTableGroup instanceof PluralTableGroup ) { + final CollectionPart.Nature nature = CollectionPart.Nature.fromName( + parentPath.getNavigablePath().getLocalName() + ); + assert nature != null; + newTableGroup = ( (PluralTableGroup) createdParentTableGroup ).getTableGroup( nature ); + } + else { + newTableGroup = getActualTableGroup( + createTableGroup( createdParentTableGroup, parentPath ), + sqmPath + ); } - final TableGroup newTableGroup = createTableGroup( parentTableGroup, (SqmPath) parentPath ); if ( newTableGroup != null ) { implicitJoinChecker.accept( newTableGroup ); - if ( sqmPath instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) sqmPath, newTableGroup ); - } + registerPathAttributeEntityNameUsage( sqmPath, newTableGroup ); } return newTableGroup; } else if ( sqmPath instanceof SqmTreatedPath ) { - fromClauseIndex.register( (SqmPath) sqmPath, tableGroup ); - if ( sqmPath instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) sqmPath, tableGroup ); - } - } - else if ( parentPath instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) parentPath, tableGroup ); + fromClauseIndex.register( sqmPath, parentTableGroup ); } if ( parentPath instanceof SqmSimplePath && CollectionPart.Nature.fromName( parentPath.getNavigablePath().getLocalName() ) == null && getCurrentClauseStack().getCurrent() != Clause.SELECT && parentPath.getParentPath() != null - && tableGroup.getModelPart() instanceof ToOneAttributeMapping ) { + && parentTableGroup.getModelPart() instanceof ToOneAttributeMapping ) { // we need to handle the case of an implicit path involving a to-one // association that path has been previously joined using left. // typically, this indicates that the to-one is being // fetched - the fetch would use a left-join. however, since the path is // used outside the select-clause also, we need to force the join to be inner - final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) tableGroup.getModelPart(); + final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) parentTableGroup.getModelPart(); final String partName = sqmPath.getResolvedModel().getPathName(); final ModelPart pathPart; if ( !toOneAttributeMapping.isFkOptimizationAllowed() @@ -3390,7 +3558,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base || !toOneAttributeMapping.getForeignKeyDescriptor().isKeyPart( (ValuedModelPart) pathPart ) ) { final NavigablePath parentParentPath = parentPath.getParentPath().getNavigablePath(); final TableGroup parentParentTableGroup = fromClauseIndex.findTableGroup( parentParentPath ); - final TableGroupJoin tableGroupJoin = parentParentTableGroup.findTableGroupJoin( tableGroup ); + final TableGroupJoin tableGroupJoin = parentParentTableGroup.findTableGroupJoin( parentTableGroup ); // We might get null here if the parentParentTableGroup is correlated and tableGroup is from the outer query // In this case, we don't want to override the join type, though it is debatable if it's ok to reuse a join in this case if ( tableGroupJoin != null ) { @@ -3398,8 +3566,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } } + registerPathAttributeEntityNameUsage( sqmPath, parentTableGroup ); - return tableGroup; + return parentTableGroup; } private void prepareForSelection(SqmPath selectionPath) { @@ -3430,23 +3599,22 @@ public abstract class BaseSqmToSqlAstConverter extends Base navigablePath = path.getLhs().getNavigablePath(); } final TableGroup createdTableGroup = createTableGroup( - fromClauseIndex.getTableGroup( navigablePath ), + getActualTableGroup( fromClauseIndex.getTableGroup( navigablePath ), path ), path ); if ( createdTableGroup != null ) { if ( path instanceof SqmTreatedPath ) { fromClauseIndex.register( path, createdTableGroup ); } - if ( path instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) path, createdTableGroup ); - } + registerEntityNameProjectionUsage( path, createdTableGroup ); } } + else { + registerEntityNameProjectionUsage( path, fromClauseIndex.findTableGroup( path.getNavigablePath() ) ); + } } else { - if ( path instanceof SqmFrom ) { - registerTreatUsage( (SqmFrom) path, tableGroup ); - } + registerEntityNameProjectionUsage( path, tableGroup ); if ( path instanceof SqmSimplePath && CollectionPart.Nature.fromName( path.getNavigablePath().getLocalName() ) == null ) { // If a table group for a selection already exists, we must make sure that the join type is INNER fromClauseIndex.findTableGroup( path.getNavigablePath().getParent() ) @@ -3457,10 +3625,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base } private TableGroup createTableGroup(TableGroup parentTableGroup, SqmPath joinedPath) { - final TableGroup actualParentTableGroup = findActualTableGroup( parentTableGroup, joinedPath ); final SqmPath lhsPath = joinedPath.getLhs(); final FromClauseIndex fromClauseIndex = getFromClauseIndex(); - final ModelPart subPart = actualParentTableGroup.getModelPart().findSubPart( + final ModelPart subPart = parentTableGroup.getModelPart().findSubPart( joinedPath.getReferencedPathSource().getPathName(), lhsPath instanceof SqmTreatedPath ? resolveEntityPersister( ( (SqmTreatedPath) lhsPath ).getTreatTarget() ) @@ -3470,13 +3637,13 @@ public abstract class BaseSqmToSqlAstConverter extends Base final TableGroup tableGroup; if ( subPart instanceof TableGroupJoinProducer ) { final TableGroupJoinProducer joinProducer = (TableGroupJoinProducer) subPart; - if ( fromClauseIndex.findTableGroupOnCurrentFromClause( actualParentTableGroup.getNavigablePath() ) == null - && !isRecursiveCte( actualParentTableGroup ) ) { + if ( fromClauseIndex.findTableGroupOnCurrentFromClause( parentTableGroup.getNavigablePath() ) == null + && !isRecursiveCte( parentTableGroup ) ) { final QuerySpec querySpec = currentQuerySpec(); // The parent table group is on a parent query, so we need a root table group tableGroup = joinProducer.createRootTableGroupJoin( joinedPath.getNavigablePath(), - actualParentTableGroup, + parentTableGroup, null, null, null, @@ -3490,14 +3657,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base } else { // Check if we can reuse a table group join of the parent - final TableGroup compatibleTableGroup = actualParentTableGroup.findCompatibleJoinedGroup( + final TableGroup compatibleTableGroup = parentTableGroup.findCompatibleJoinedGroup( joinProducer, SqlAstJoinType.INNER ); if ( compatibleTableGroup == null ) { final TableGroupJoin tableGroupJoin = joinProducer.createTableGroupJoin( joinedPath.getNavigablePath(), - actualParentTableGroup, + parentTableGroup, null, null, null, @@ -3512,10 +3679,10 @@ public abstract class BaseSqmToSqlAstConverter extends Base final boolean nested = currentClauseStack.getCurrent() == Clause.FROM && currentlyProcessingJoin instanceof SqmAttributeJoin; if ( nested ) { - actualParentTableGroup.addNestedTableGroupJoin( tableGroupJoin ); + parentTableGroup.addNestedTableGroupJoin( tableGroupJoin ); } else { - actualParentTableGroup.addTableGroupJoin( tableGroupJoin ); + parentTableGroup.addTableGroupJoin( tableGroupJoin ); } tableGroup = tableGroupJoin.getJoinedGroup(); } @@ -3544,31 +3711,6 @@ public abstract class BaseSqmToSqlAstConverter extends Base return false; } - private TableGroup findCompatibleJoinedGroup( - TableGroup parentTableGroup, - TableGroupJoinProducer joinProducer, - SqlAstJoinType requestedJoinType) { - // We don't look into nested table group joins as that wouldn't be "compatible" - for ( TableGroupJoin join : parentTableGroup.getTableGroupJoins() ) { - // Compatibility obviously requires the same model part but also join type compatibility - // Note that if the requested join type is left, we can also use an existing inner join - // The other case, when the requested join type is inner and there is an existing left join, - // is not compatible though because the cardinality is different. - // We could reuse the join though if we alter the join type to INNER, but that's an optimization for later - final SqlAstJoinType joinType = join.getJoinType(); - if ( join.getJoinedGroup().getModelPart() == joinProducer - && ( requestedJoinType == joinType || requestedJoinType == SqlAstJoinType.LEFT && joinType == SqlAstJoinType.INNER ) ) { - // If there is an existing inner join, we can always use that as a new join can never produce results - // regardless of the join type or predicate since the LHS is the same table group - // If this is a left join though, we have to check if the predicate is simply the association predicate - if ( joinType == SqlAstJoinType.INNER || joinProducer.isSimpleJoinPredicate( join.getPredicate() ) ) { - return join.getJoinedGroup(); - } - } - } - return null; - } - private void registerPluralTableGroupParts(TableGroup tableGroup) { registerPluralTableGroupParts( null, tableGroup ); } @@ -4766,58 +4908,166 @@ public abstract class BaseSqmToSqlAstConverter extends Base // A case wrapper for non-basic paths is not possible, // because a case expression must return a scalar value, // so we instead add the type restriction predicate as conjunct + // by registering the treat into tableGroupEntityNameUses final String treatedName = treatedPath.getTreatTarget().getHibernateEntityName(); final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( treatedName ); - conjunctTreatUsages - .computeIfAbsent( wrappedPath, p -> new HashSet<>( 1 ) ) - .addAll( entityDescriptor.getSubclassEntityNames() ); + final TableGroup tableGroup = getFromClauseIndex().findTableGroup( wrappedPath.getNavigablePath() ); + final TableGroup actualTableGroup; + if ( tableGroup instanceof PluralTableGroup ) { + final CollectionPart.Nature nature = CollectionPart.Nature.fromName( path.getNavigablePath().getLocalName() ); + actualTableGroup = ( (PluralTableGroup) tableGroup ).getTableGroup( + nature == null + ? CollectionPart.Nature.ELEMENT + : nature + ); + } + else { + actualTableGroup = tableGroup; + } + final Map entityNameUses = tableGroupEntityNameUses.computeIfAbsent( + actualTableGroup, + p -> new HashMap<>( 1 ) + ); + for ( String subclassEntityName : entityDescriptor.getSubclassEntityNames() ) { + entityNameUses.compute( + subclassEntityName, + (s, existingUse) -> EntityNameUse.TREAT.stronger( existingUse ) + ); + } return expression; } - if ( wrappedPath instanceof EntityDiscriminatorSqmPath ) { - // Note: If the columns that are accessed are not shared with other entities, we could avoid this wrapping + final BasicValuedPathInterpretation basicPath = (BasicValuedPathInterpretation) expression; + final AbstractEntityPersister persister = (AbstractEntityPersister) basicPath.getTableGroup().getModelPart().getPartMappingType(); + // Only need a case expression around the basic valued path for the parent treat expression + // if the column of the basic valued path is shared between subclasses + if ( persister.isSharedColumn( basicPath.getColumnReference().getColumnExpression() ) ) { return createCaseExpression( wrappedPath, treatedPath.getTreatTarget(), expression ); } - return expression; } return expression; } private Expression createCaseExpression(SqmPath lhs, EntityDomainType treatTarget, Expression expression) { + final Predicate treatTypeRestriction = createTreatTypeRestriction( lhs, treatTarget ); + if ( treatTypeRestriction == null ) { + return expression; + } final BasicValuedMapping mappingModelExpressible = (BasicValuedMapping) expression.getExpressionType(); final List whenFragments = new ArrayList<>( 1 ); whenFragments.add( new CaseSearchedExpression.WhenFragment( - createTreatTypeRestriction( lhs, treatTarget ), + treatTypeRestriction, expression ) ); return new CaseSearchedExpression( mappingModelExpressible, whenFragments, - new QueryLiteral<>( null, mappingModelExpressible ) + null ); } private Predicate consumeConjunctTreatTypeRestrictions() { - return consumeConjunctTreatTypeRestrictions( conjunctTreatUsages ); + return consumeConjunctTreatTypeRestrictions( tableGroupEntityNameUses ); } - private Predicate consumeConjunctTreatTypeRestrictions(Map, Set> conjunctTreatUsages) { + private Predicate consumeConjunctTreatTypeRestrictions(Map> conjunctTreatUsages) { if ( conjunctTreatUsages == null || conjunctTreatUsages.isEmpty() ) { return null; } Predicate predicate = null; - for ( Map.Entry, Set> entry : conjunctTreatUsages.entrySet() ) { + for ( Map.Entry> entry : conjunctTreatUsages.entrySet() ) { + final TableGroup tableGroup = entry.getKey(); + final Set entityNames = determineEntityNamesForTreatTypeRestriction( + (EntityMappingType) tableGroup.getModelPart().getPartMappingType(), + entry.getValue() + ); + if ( entityNames.isEmpty() ) { + continue; + } + registerTypeUsage( tableGroup ); + + final ModelPartContainer modelPart = tableGroup.getModelPart(); + final EntityMappingType entityMapping; + if ( modelPart instanceof EntityValuedModelPart ) { + entityMapping = ( (EntityValuedModelPart) modelPart ).getEntityMappingType(); + } + else { + entityMapping = (EntityMappingType) ( (PluralAttributeMapping) modelPart ).getElementDescriptor().getPartMappingType(); + } + + final DiscriminatorPathInterpretation typeExpression = new DiscriminatorPathInterpretation<>( + tableGroup.getNavigablePath().append( EntityDiscriminatorMapping.ROLE_NAME ), + entityMapping, + tableGroup, + this + ); + registerTypeUsage( tableGroup ); predicate = combinePredicates( predicate, - createTreatTypeRestriction( entry.getKey(), entry.getValue() ) + createTreatTypeRestriction( + typeExpression, + entityNames + ) ); } - conjunctTreatUsages.clear(); return predicate; } + private Set determineEntityNamesForTreatTypeRestriction( + EntityMappingType partMappingType, + Map entityNameUses) { + final Set entityNameUsesSet = entityNameUses.keySet(); + if ( entityNameUsesSet.containsAll( partMappingType.getSubclassEntityNames() ) ) { + // No need to create a restriction if all subclasses are used + return Collections.emptySet(); + } + if ( entityNameUses.containsValue( EntityNameUse.FILTER ) ) { + // If the conjunct contains FILTER uses we can omit the treat type restriction + return Collections.emptySet(); + } + final String baseEntityNameToAdd; + if ( entityNameUses.containsKey( partMappingType.getEntityName() ) ) { + if ( !partMappingType.isAbstract() ) { + baseEntityNameToAdd = partMappingType.getEntityName(); + } + else { + baseEntityNameToAdd = null; + } + if ( entityNameUses.size() == 1 ) { + return Collections.emptySet(); + } + } + else { + baseEntityNameToAdd = null; + } + final Set entityNames = new HashSet<>( entityNameUsesSet.size() ); + for ( Map.Entry entityNameUse : entityNameUses.entrySet() ) { + if ( entityNameUse.getValue() == EntityNameUse.TREAT ) { + final String entityName = entityNameUse.getKey(); + final EntityPersister entityDescriptor = creationContext.getMappingMetamodel() + .findEntityDescriptor( entityName ); + if ( !entityDescriptor.isAbstract() ) { + entityNames.add( entityDescriptor.getEntityName() ); + } + for ( EntityMappingType subMappingType : entityDescriptor.getSubMappingTypes() ) { + if ( !subMappingType.isAbstract() ) { + entityNames.add( subMappingType.getEntityName() ); + } + } + } + } + do { + entityNames.remove( partMappingType.getEntityName() ); + partMappingType = partMappingType.getSuperMappingType(); + } while ( partMappingType != null ); + if ( !entityNames.isEmpty() && baseEntityNameToAdd != null ) { + entityNames.add( baseEntityNameToAdd ); + } + return entityNames; + } + private Predicate createTreatTypeRestriction(SqmPath lhs, EntityDomainType treatTarget) { final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( treatTarget.getHibernateEntityName() ); final Set subclassEntityNames = entityDescriptor.getSubclassEntityNames(); @@ -4829,7 +5079,13 @@ public abstract class BaseSqmToSqlAstConverter extends Base // as that would register a type usage for the table group that we don't want here final EntityDiscriminatorSqmPath discriminatorSqmPath = (EntityDiscriminatorSqmPath) lhs.type(); registerTypeUsage( discriminatorSqmPath ); - final Expression typeExpression = DiscriminatorPathInterpretation.from( discriminatorSqmPath, this ); + return createTreatTypeRestriction( + DiscriminatorPathInterpretation.from( discriminatorSqmPath, this ), + subclassEntityNames + ); + } + + private Predicate createTreatTypeRestriction(Expression typeExpression, Set subclassEntityNames) { if ( subclassEntityNames.size() == 1 ) { return new ComparisonPredicate( typeExpression, @@ -6526,21 +6782,37 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public Predicate visitNestedTopLevelPredicate(SqmPredicate predicate) { - final Map, Set> originalConjunctTableGroupTreatUsages; - if ( conjunctTreatUsages.isEmpty() ) { + final Map> originalConjunctTableGroupTreatUsages; + if ( tableGroupEntityNameUses.isEmpty() ) { originalConjunctTableGroupTreatUsages = null; } else { - originalConjunctTableGroupTreatUsages = new IdentityHashMap<>( conjunctTreatUsages ); + originalConjunctTableGroupTreatUsages = new IdentityHashMap<>( tableGroupEntityNameUses ); } - conjunctTreatUsages.clear(); + tableGroupEntityNameUses.clear(); final Predicate result = (Predicate) predicate.accept( this ); final Predicate finalPredicate = combinePredicates( result, consumeConjunctTreatTypeRestrictions() ); if ( originalConjunctTableGroupTreatUsages != null ) { - conjunctTreatUsages.putAll( originalConjunctTableGroupTreatUsages ); + for ( Map.Entry> entry : originalConjunctTableGroupTreatUsages.entrySet() ) { + final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( + entry.getKey(), + entry.getValue() + ); + if ( entityNameUses != null && entityNameUses != entry.getValue() ) { + for ( Map.Entry useEntry : entry.getValue().entrySet() ) { + final EntityNameUse currentUseKind = entityNameUses.get( useEntry.getKey() ); + if ( currentUseKind == null ) { + entityNameUses.put( useEntry.getKey(), useEntry.getValue() ); + } + else { + entityNameUses.put( useEntry.getKey(), useEntry.getValue().stronger( currentUseKind ) ); + } + } + } + } } return finalPredicate; } @@ -6564,48 +6836,74 @@ public abstract class BaseSqmToSqlAstConverter extends Base new ArrayList<>( predicate.getPredicates().size() ), getBooleanType() ); - final List, Set>> conjunctTreatUsagesList = new ArrayList<>( predicate.getPredicates().size() ); - final Map, Set> conjunctTreatUsagesUnion = new IdentityHashMap<>(); - boolean hasAnyTreatUsage = false; - for ( SqmPredicate subPredicate : predicate.getPredicates() ) { - disjunction.add( (Predicate) subPredicate.accept( this ) ); - if ( !conjunctTreatUsages.isEmpty() ) { - hasAnyTreatUsage = true; - for ( Map.Entry, Set> entry : conjunctTreatUsages.entrySet() ) { - conjunctTreatUsagesUnion.computeIfAbsent( entry.getKey(), k -> new HashSet<>() ) - .addAll( entry.getValue() ); + final Map> originalConjunctTableGroupTreatUsages; + if ( tableGroupEntityNameUses.isEmpty() ) { + originalConjunctTableGroupTreatUsages = null; + } + else { + originalConjunctTableGroupTreatUsages = new IdentityHashMap<>( tableGroupEntityNameUses ); + } + Map>[] conjunctTreatUsagesArray = null; + Map> conjunctTreatUsagesUnion = null; + List predicates = predicate.getPredicates(); + for ( int i = 0; i < predicates.size(); i++ ) { + tableGroupEntityNameUses.clear(); + disjunction.add( (Predicate) predicates.get( i ).accept( this ) ); + if ( !tableGroupEntityNameUses.isEmpty() ) { + if ( conjunctTreatUsagesArray == null ) { + conjunctTreatUsagesArray = new Map[predicate.getPredicates().size()]; + conjunctTreatUsagesUnion = new IdentityHashMap<>(); } - conjunctTreatUsagesList.add( new IdentityHashMap<>( conjunctTreatUsages ) ); - conjunctTreatUsages.clear(); + for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { + final Map entityNameUses = conjunctTreatUsagesUnion.computeIfAbsent( + entry.getKey(), + k -> new HashMap<>() + ); + entityNameUses.putAll( entry.getValue() ); + } + conjunctTreatUsagesArray[i] = new IdentityHashMap<>( tableGroupEntityNameUses ); } } - if ( !hasAnyTreatUsage ) { + if ( conjunctTreatUsagesArray == null ) { + if ( originalConjunctTableGroupTreatUsages != null ) { + tableGroupEntityNameUses.putAll( originalConjunctTableGroupTreatUsages ); + } return disjunction; } // Build the intersection of the conjunct treat usages, // so that we can push that up and infer during pruning, which entity subclasses can be omitted - conjunctTreatUsages.putAll( conjunctTreatUsagesUnion ); - - final Iterator, Set>> iterator = conjunctTreatUsages.entrySet().iterator(); + final Iterator>> iterator = tableGroupEntityNameUses.entrySet().iterator(); while ( iterator.hasNext() ) { - final Map.Entry, Set> entry = iterator.next(); - final Set intersected = new HashSet<>( entry.getValue() ); + final Map.Entry> entry = iterator.next(); + final Map intersected = new HashMap<>( entry.getValue() ); entry.setValue( intersected ); boolean remove = false; - for ( Map, Set> conjunctTreatUsages : conjunctTreatUsagesList ) { - final Set entityNames = conjunctTreatUsages.get( entry.getKey() ); - if ( entityNames == null ) { + for ( Map> conjunctTreatUsages : conjunctTreatUsagesArray ) { + final Map entityNames; + if ( conjunctTreatUsages == null || ( entityNames = conjunctTreatUsages.get( entry.getKey() ) ) == null ) { remove = true; continue; } // Intersect the two sets and transfer the common elements to the intersection - intersected.retainAll( entityNames ); + final Iterator> intersectedIter = intersected.entrySet().iterator(); + while ( intersectedIter.hasNext() ) { + final Map.Entry intersectedEntry = intersectedIter.next(); + final EntityNameUse intersectedUseKind = intersectedEntry.getValue(); + final EntityNameUse useKind = entityNames.get( intersectedEntry.getKey() ); + if ( useKind == null ) { + intersectedIter.remove(); + } + else { + // Possibly downgrade a FILTER use to EXPRESSION if one of the disjunctions does not use FILTER + intersectedEntry.setValue( intersectedUseKind.weaker( useKind ) ); + } + } if ( intersected.isEmpty() ) { remove = true; continue; } - entityNames.removeAll( intersected ); + entityNames.keySet().removeAll( intersected.keySet() ); if ( entityNames.isEmpty() ) { conjunctTreatUsages.remove( entry.getKey() ); } @@ -6617,9 +6915,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base } // Prepend the treat type usages to the respective conjuncts - for ( int i = 0; i < conjunctTreatUsagesList.size(); i++ ) { - final Map, Set> conjunctTreatUsages = conjunctTreatUsagesList.get( i ); - if ( conjunctTreatUsages != null ) { + for ( int i = 0; i < conjunctTreatUsagesArray.length; i++ ) { + final Map> conjunctTreatUsages = conjunctTreatUsagesArray[i]; + if ( conjunctTreatUsages != null && !conjunctTreatUsages.isEmpty() ) { disjunction.getPredicates().set( i, combinePredicates( @@ -6629,6 +6927,25 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); } } + if ( originalConjunctTableGroupTreatUsages != null ) { + for ( Map.Entry> entry : originalConjunctTableGroupTreatUsages.entrySet() ) { + final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( + entry.getKey(), + entry.getValue() + ); + if ( entityNameUses != null && entityNameUses != entry.getValue() ) { + for ( Map.Entry useEntry : entry.getValue().entrySet() ) { + final EntityNameUse currentUseKind = entityNameUses.get( useEntry.getKey() ); + if ( currentUseKind == null ) { + entityNameUses.put( useEntry.getKey(), useEntry.getValue() ); + } + else { + entityNameUses.put( useEntry.getKey(), useEntry.getValue().stronger( currentUseKind ) ); + } + } + } + } + } return disjunction; } @@ -6743,9 +7060,69 @@ public abstract class BaseSqmToSqlAstConverter extends Base final ComparisonOperator sqmOperator = predicate.isNegated() ? predicate.getSqmOperator().negated() : predicate.getSqmOperator(); + handleTypeComparison( lhs, rhs, sqmOperator == ComparisonOperator.EQUAL ); return new ComparisonPredicate( lhs, sqmOperator, rhs, getBooleanType() ); } + private void handleTypeComparison(Expression lhs, Expression rhs, boolean inclusive) { + final DiscriminatorPathInterpretation typeExpression; + final EntityTypeLiteral literalExpression; + if ( lhs instanceof DiscriminatorPathInterpretation ) { + typeExpression = (DiscriminatorPathInterpretation) lhs; + literalExpression = rhs instanceof EntityTypeLiteral ? (EntityTypeLiteral) rhs : null; + } + else if ( rhs instanceof DiscriminatorPathInterpretation ) { + typeExpression = (DiscriminatorPathInterpretation) rhs; + literalExpression = lhs instanceof EntityTypeLiteral ? (EntityTypeLiteral) lhs : null; + } + else { + return; + } + if ( literalExpression == null ) { + // We have to assume all types are possible and can't do optimizations + final TableGroup tableGroup = getFromClauseIndex().getTableGroup( typeExpression.getNavigablePath().getParent() ); + final EntityMappingType entityMappingType = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, entityMappingType.getEntityName() ); + for ( EntityMappingType subMappingType : entityMappingType.getSubMappingTypes() ) { + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, subMappingType.getEntityName() ); + } + } + else { + handleTypeComparison( typeExpression, Collections.singletonList( literalExpression ), inclusive ); + } + } + + private void handleTypeComparison( + DiscriminatorPathInterpretation typeExpression, + List literalExpressions, + boolean inclusive) { + final TableGroup tableGroup = getFromClauseIndex().getTableGroup( typeExpression.getNavigablePath().getParent() ); + if ( inclusive ) { + for ( EntityTypeLiteral literalExpr : literalExpressions ) { + registerEntityNameUsage( + tableGroup, + EntityNameUse.FILTER, + literalExpr.getEntityTypeDescriptor().getEntityName() + ); + } + } + else { + final EntityMappingType entityMappingType = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); + final Set excludedEntityNames = new HashSet<>(entityMappingType.getSubMappingTypes().size()); + for ( EntityTypeLiteral literalExpr : literalExpressions ) { + excludedEntityNames.add( literalExpr.getEntityTypeDescriptor().getEntityName() ); + } + if ( !excludedEntityNames.contains( entityMappingType.getEntityName() ) ) { + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, entityMappingType.getEntityName() ); + } + for ( EntityMappingType subMappingType : entityMappingType.getSubMappingTypes() ) { + if ( !excludedEntityNames.contains( subMappingType.getEntityName() ) ) { + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, subMappingType.getEntityName() ); + } + } + } + } + @Override public Object visitIsEmptyPredicate(SqmEmptinessPredicate predicate) { prepareReusablePath( predicate.getPluralPath(), () -> null ); @@ -6753,10 +7130,12 @@ public abstract class BaseSqmToSqlAstConverter extends Base final QuerySpec subQuerySpec = new QuerySpec( false, 1 ); final FromClauseAccess parentFromClauseAccess = getFromClauseAccess(); - final SqlAstProcessingStateImpl subQueryState = new SqlAstProcessingStateImpl( + final SqlAstQueryPartProcessingStateImpl subQueryState = new SqlAstQueryPartProcessingStateImpl( + subQuerySpec, getCurrentProcessingState(), this, - currentClauseStack::getCurrent + currentClauseStack::getCurrent, + true ); pushProcessingState( subQueryState ); @@ -6906,8 +7285,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base final SqmParameter sqmParameter = (SqmParameter) sqmExpression; if ( sqmParameter.allowMultiValuedBinding() ) { - final Predicate specialCase = processInListWithSingleParameter( predicate, sqmParameter ); + final InListPredicate specialCase = processInListWithSingleParameter( predicate, sqmParameter ); if ( specialCase != null ) { + handleTypeComparison( specialCase ); return specialCase; } } @@ -6951,11 +7331,45 @@ public abstract class BaseSqmToSqlAstConverter extends Base finally { inferrableTypeAccessStack.pop(); } - + handleTypeComparison( inPredicate ); return inPredicate; } - private Predicate processInListWithSingleParameter( + private void handleTypeComparison(InListPredicate inPredicate) { + final Expression testExpression = inPredicate.getTestExpression(); + if ( testExpression instanceof DiscriminatorPathInterpretation ) { + final DiscriminatorPathInterpretation typeExpression = (DiscriminatorPathInterpretation) testExpression; + boolean containsNonLiteral = false; + for ( Expression listExpression : inPredicate.getListExpressions() ) { + if ( !( listExpression instanceof EntityTypeLiteral ) ) { + containsNonLiteral = true; + break; + } + } + if ( containsNonLiteral ) { + // We have to assume all types are possible and can't do optimizations + final TableGroup tableGroup = getFromClauseIndex().getTableGroup( + typeExpression.getNavigablePath().getParent() + ); + final EntityMappingType entityMappingType = (EntityMappingType) tableGroup.getModelPart() + .getPartMappingType(); + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, entityMappingType.getEntityName() ); + for ( EntityMappingType subMappingType : entityMappingType.getSubMappingTypes() ) { + registerEntityNameUsage( tableGroup, EntityNameUse.FILTER, subMappingType.getEntityName() ); + } + } + else { + //noinspection unchecked + handleTypeComparison( + typeExpression, + (List) (List) inPredicate.getListExpressions(), + !inPredicate.isNegated() + ); + } + } + } + + private InListPredicate processInListWithSingleParameter( SqmInListPredicate sqmPredicate, SqmParameter sqmParameter) { assert sqmParameter.allowMultiValuedBinding(); @@ -6967,7 +7381,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base return processInSingleHqlParameter( sqmPredicate, sqmParameter ); } - private Predicate processInSingleHqlParameter(SqmInListPredicate sqmPredicate, SqmParameter sqmParameter) { + private InListPredicate processInSingleHqlParameter(SqmInListPredicate sqmPredicate, SqmParameter sqmParameter) { final QueryParameterImplementor domainParam = domainParameterXref.getQueryParameter( sqmParameter ); final QueryParameterBinding domainParamBinding = domainParameterBindings.getBinding( domainParam ); if ( !domainParamBinding.isMultiValued() ) { @@ -6978,7 +7392,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base return processInSingleParameter( sqmPredicate, sqmParameter, domainParam, domainParamBinding ); } - private Predicate processInSingleCriteriaParameter( + private InListPredicate processInSingleCriteriaParameter( SqmInListPredicate sqmPredicate, JpaCriteriaParameter jpaCriteriaParameter) { assert jpaCriteriaParameter.allowsMultiValuedBinding(); @@ -6993,7 +7407,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base } @SuppressWarnings( "rawtypes" ) - private Predicate processInSingleParameter( + private InListPredicate processInSingleParameter( SqmInListPredicate sqmPredicate, SqmParameter sqmParameter, QueryParameterImplementor domainParam, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java index 79f2635505..3f1515bf1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java @@ -14,10 +14,13 @@ import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.spi.NavigablePath; import org.hibernate.query.SemanticException; import org.hibernate.query.sqm.SemanticQueryWalker; @@ -45,25 +48,37 @@ public class BasicValuedPathInterpretation extends AbstractSqmPathInterpretat public static BasicValuedPathInterpretation from( SqmBasicValuedSimplePath sqmPath, SqlAstCreationState sqlAstCreationState, - SemanticQueryWalker sqmWalker, + SemanticQueryWalker sqmWalker, boolean jpaQueryComplianceEnabled, Clause currentClause) { final FromClauseAccess fromClauseAccess = sqlAstCreationState.getFromClauseAccess(); final TableGroup tableGroup = fromClauseAccess.getTableGroup( sqmPath.getNavigablePath().getParent() ); + final SqmPath lhs = sqmPath.getLhs(); EntityMappingType treatTarget = null; - if ( jpaQueryComplianceEnabled ) { - if ( sqmPath.getLhs() instanceof SqmTreatedPath ) { - final EntityDomainType treatTargetDomainType = ( (SqmTreatedPath) sqmPath.getLhs() ).getTreatTarget(); + final ModelPartContainer modelPartContainer; + if ( lhs instanceof SqmTreatedPath ) { + final EntityDomainType treatTargetDomainType = ( (SqmTreatedPath) lhs ).getTreatTarget(); - final MappingMetamodel mappingMetamodel = sqlAstCreationState.getCreationContext() - .getSessionFactory() - .getRuntimeMetamodels() - .getMappingMetamodel(); - treatTarget = mappingMetamodel.findEntityDescriptor( treatTargetDomainType.getHibernateEntityName() ); + final MappingMetamodel mappingMetamodel = sqlAstCreationState.getCreationContext() + .getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final EntityPersister treatEntityDescriptor = mappingMetamodel.findEntityDescriptor( treatTargetDomainType.getHibernateEntityName() ); + final MappingType tableGroupMappingType = tableGroup.getModelPart().getPartMappingType(); + if ( tableGroupMappingType instanceof EntityMappingType + && treatEntityDescriptor.isTypeOrSuperType( (EntityMappingType) tableGroupMappingType ) ) { + modelPartContainer = tableGroup.getModelPart(); + treatTarget = treatEntityDescriptor; } - else if ( sqmPath.getLhs().getNodeType() instanceof EntityDomainType ) { - final EntityDomainType entityDomainType = (EntityDomainType) sqmPath.getLhs().getNodeType(); + else { + modelPartContainer = treatEntityDescriptor; + } + } + else { + modelPartContainer = tableGroup.getModelPart(); + if ( jpaQueryComplianceEnabled && lhs.getNodeType() instanceof EntityDomainType ) { + final EntityDomainType entityDomainType = (EntityDomainType) lhs.getNodeType(); final MappingMetamodel mappingMetamodel = sqlAstCreationState.getCreationContext() .getSessionFactory() .getRuntimeMetamodels() @@ -72,20 +87,19 @@ public class BasicValuedPathInterpretation extends AbstractSqmPathInterpretat } } - final ModelPartContainer modelPart = tableGroup.getModelPart(); final BasicValuedModelPart mapping; // In the select, group by, order by and having clause we have to make sure we render the column of the target table, // never the FK column, if the lhs is a SqmFrom i.e. something explicitly queried/joined. if ( ( currentClause == Clause.GROUP || currentClause == Clause.SELECT || currentClause == Clause.ORDER || currentClause == Clause.HAVING ) && sqmPath.getLhs() instanceof SqmFrom - && modelPart.getPartMappingType() instanceof ManagedMappingType ) { - mapping = (BasicValuedModelPart) ( (ManagedMappingType) modelPart.getPartMappingType() ).findSubPart( + && modelPartContainer.getPartMappingType() instanceof ManagedMappingType ) { + mapping = (BasicValuedModelPart) ( (ManagedMappingType) modelPartContainer.getPartMappingType() ).findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget ); } else { - mapping = (BasicValuedModelPart) modelPart.findSubPart( + mapping = (BasicValuedModelPart) modelPartContainer.findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget ); @@ -153,6 +167,11 @@ public class BasicValuedPathInterpretation extends AbstractSqmPathInterpretat return columnReference; } + @Override + public ColumnReference getColumnReference() { + return columnReference; + } + @Override public void accept(SqlAstWalker sqlTreeWalker) { columnReference.accept( sqlTreeWalker ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java index e91a652c30..a8259f828b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; @@ -19,11 +20,11 @@ import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.results.graph.FetchParent; -import org.hibernate.sql.results.graph.FetchableContainer; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.TypeConfiguration; @@ -35,6 +36,7 @@ public class SqlAstQueryPartProcessingStateImpl implements SqlAstQueryPartProcessingState { private final QueryPart queryPart; + private final Map, Boolean>> treatRegistrations = new HashMap<>(); private final boolean deduplicateSelectionItems; private FetchParent nestingFetchParent; @@ -74,6 +76,30 @@ public class SqlAstQueryPartProcessingStateImpl return queryPart; } + @Override + public void registerTreat(TableGroup tableGroup, EntityDomainType treatType) { + treatRegistrations.computeIfAbsent( tableGroup, tg -> new HashMap<>() ).put( treatType, Boolean.FALSE ); + } + + @Override + public void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType) { + final Map, Boolean> treatUses = treatRegistrations.get( tableGroup ); + if ( treatUses == null ) { + final SqlAstProcessingState parentState = getParentState(); + if ( parentState instanceof SqlAstQueryPartProcessingState ) { + ( (SqlAstQueryPartProcessingState) parentState ).registerTreatUsage( tableGroup, treatType ); + } + } + else { + treatUses.put( treatType, Boolean.TRUE ); + } + } + + @Override + public Map, Boolean>> getTreatRegistrations() { + return treatRegistrations; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SqlExpressionResolver diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java index 681d2feb92..861780d7f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.tree.domain; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -502,6 +503,11 @@ public class SqmPolymorphicRootDescriptor implements EntityDomainType { throw new UnsupportedOperationException( ); } + @Override + public Collection> getSubTypes() { + throw new UnsupportedOperationException( ); + } + @Override public void addSubType(ManagedDomainType subType) { throw new UnsupportedOperationException( ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/InFragment.java b/hibernate-core/src/main/java/org/hibernate/sql/InFragment.java index cc2e362bc1..d41a3cfd7d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/InFragment.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/InFragment.java @@ -57,6 +57,10 @@ public class InFragment { return setColumn( this.columnName ); } + public List getValues() { + return values; + } + public String toFragmentString() { if ( values.size() == 0 ) { return "1=2"; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java index 904ea3e930..89a46ff154 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java @@ -6,8 +6,11 @@ */ package org.hibernate.sql.ast.spi; +import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.persister.entity.EntityNameUse; +import org.hibernate.sql.ast.tree.from.TableGroup; /** * Access to stuff used while creating a SQL AST @@ -28,4 +31,15 @@ public interface SqlAstCreationState { LoadQueryInfluencers getLoadQueryInfluencers(); void registerLockMode(String identificationVariable, LockMode explicitLockMode); + + /** + * This callback is for handling of filters and is necessary to allow correct treat optimizations. + */ + @Internal + default void registerEntityNameUsage( + TableGroup tableGroup, + EntityNameUse entityNameUse, + String hibernateEntityName) { + // No-op + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java index 5f90ca5253..3cfb1fade8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java @@ -6,10 +6,15 @@ */ package org.hibernate.sql.ast.spi; +import java.util.Map; + +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; /** - * SqlAstProcessingState specialization for + * SqlAstProcessingState specialization for query parts + * * @author Steve Ebersole */ public interface SqlAstQueryPartProcessingState extends SqlAstProcessingState { @@ -18,4 +23,13 @@ public interface SqlAstQueryPartProcessingState extends SqlAstProcessingState { * considered in-flight as it is probably still being built. */ QueryPart getInflightQueryPart(); + + void registerTreat(TableGroup tableGroup, EntityDomainType treatType); + + void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType); + + /** + * The treat registrations. The boolean indicates whether the treat is used in the query part. + */ + Map, Boolean>> getTreatRegistrations(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CorrelatedTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CorrelatedTableGroup.java index 267d28c0c0..962d3ac5aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CorrelatedTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CorrelatedTableGroup.java @@ -48,6 +48,10 @@ public class CorrelatedTableGroup extends AbstractTableGroup { this.joinPredicateConsumer = joinPredicateConsumer; } + public TableGroup getCorrelatedTableGroup() { + return correlatedTableGroup; + } + @Override public void addTableGroupJoin(TableGroupJoin join) { assert !getTableGroupJoins().contains( join ); @@ -155,8 +159,4 @@ public class CorrelatedTableGroup extends AbstractTableGroup { public Consumer getJoinPredicateConsumer() { return joinPredicateConsumer; } - - public TableGroup getCorrelatedTableGroup(){ - return correlatedTableGroup; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/PluralTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/PluralTableGroup.java index a4c23f1ddb..18b112cc00 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/PluralTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/PluralTableGroup.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.ast.tree.from; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.PluralAttributeMapping; /** @@ -18,4 +19,15 @@ public interface PluralTableGroup extends TableGroup { TableGroup getElementTableGroup(); TableGroup getIndexTableGroup(); + + default TableGroup getTableGroup(CollectionPart.Nature nature) { + switch ( nature ) { + case ELEMENT: + return getElementTableGroup(); + case INDEX: + return getIndexTableGroup(); + } + + throw new IllegalStateException( "Could not find table group for: " + nature ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index ee0a998bae..47d323020d 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -446,11 +447,12 @@ public class EntityMetamodel implements Serializable { hasOwnedCollections = foundOwnedCollection; mutablePropertiesIndexes = mutableIndexes; - final Set subclassEntityNamesLocal = new HashSet<>(); + // Need deterministic ordering + final Set subclassEntityNamesLocal = new LinkedHashSet<>(); + subclassEntityNamesLocal.add( name ); for ( Subclass subclass : persistentClass.getSubclasses() ) { subclassEntityNamesLocal.add( subclass.getEntityName() ); } - subclassEntityNamesLocal.add( name ); subclassEntityNames = toSmallSet( subclassEntityNamesLocal ); HashMap, String> entityNameByInheritanceClassMapLocal = new HashMap<>(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentJoined.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentJoined.java index 7302dcb6fa..5051c943b2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentJoined.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentJoined.java @@ -8,6 +8,9 @@ package org.hibernate.orm.test.inheritance; import java.util.Comparator; import java.util.List; + +import org.hibernate.internal.util.ExceptionHelper; + import jakarta.persistence.Column; import jakarta.persistence.ConstraintMode; import jakarta.persistence.DiscriminatorColumn; @@ -27,23 +30,24 @@ import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; 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.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.fail; -@TestForIssue(jiraKey = "HHH-14103") +@JiraKey("HHH-14103") @DomainModel( annotatedClasses = { TransientOverrideAsPersistentJoined.Employee.class, @@ -114,25 +118,24 @@ public class TransientOverrideAsPersistentJoined { } @Test - @FailureExpected(jiraKey = "HHH-12981") + @JiraKey("HHH-12981") public void testQueryByRootClassAndOverridenProperty(SessionFactoryScope scope) { scope.inTransaction( session -> { - final Employee editor = session.createQuery( "from Employee where title=:title", Employee.class ) - .setParameter( "title", "Senior Editor" ) - .getSingleResult(); - assertThat( editor, instanceOf( Editor.class ) ); - - final Employee writer = session.createQuery( "from Employee where title=:title", Employee.class ) - .setParameter( "title", "Writing" ) - .getSingleResult(); - assertThat( writer, instanceOf( Writer.class ) ); - assertNotNull( ( (Writer) writer ).getGroup() ); - assertEquals( writer.getTitle(), ( (Writer) writer ).getGroup().getName() ); + try { + session.createQuery( "from Employee where title=:title", Employee.class ); + fail( "Expected exception!" ); + } + catch (IllegalArgumentException e) { + assertThat( + ExceptionHelper.getRootCause( e ).getMessage(), + containsString( "due to the attribute being declared in multiple sub types" ) + ); + } } ); } @Test - @FailureExpected(jiraKey = "HHH-12981") + @JiraKey("HHH-12981") public void testQueryByRootClassAndOverridenPropertyTreat(SessionFactoryScope scope) { scope.inTransaction( session -> { final Employee editor = session.createQuery( @@ -172,7 +175,7 @@ public class TransientOverrideAsPersistentJoined { } @Test - @FailureExpected(jiraKey = "HHH-12981") + @JiraKey("HHH-12981") public void testCriteriaQueryByRootClassAndOverridenProperty(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java new file mode 100644 index 0000000000..163cc51c27 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java @@ -0,0 +1,527 @@ +/* + * 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.jpa.criteria; + +import org.hibernate.dialect.H2Dialect; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + EntityUseJoinedSubclassOptimizationTest.Thing.class, + EntityUseJoinedSubclassOptimizationTest.Building.class, + EntityUseJoinedSubclassOptimizationTest.House.class, + EntityUseJoinedSubclassOptimizationTest.Skyscraper.class, + EntityUseJoinedSubclassOptimizationTest.Vehicle.class, + EntityUseJoinedSubclassOptimizationTest.Car.class, + EntityUseJoinedSubclassOptimizationTest.Airplane.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +// Run only on H2 to avoid dealing with SQL dialect differences +@RequiresDialect( H2Dialect.class ) +public class EntityUseJoinedSubclassOptimizationTest { + + @Test + public void testEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + // We need to join all tables because the EntityResult will create fetches for all subtypes. + // We could optimize this by making use of tableGroupEntityNameUses in BaseSqmToSqlAstConverter#visitFetches, + // but for that to be safe, we need to call BaseSqmToSqlAstConverter#visitSelectClause last + // and make sure the select clause contains no treat expressions, as that would affect tableGroupEntityNameUses + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "join Building t1_1 on t1_0.id=t1_1.id " + + "join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select 1 from Thing t where type(t) = Building" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "1 " + + "from Thing t1_0 " + + "join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_1.id is not null then 1 " + + "end=1", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqTypeRestrictionSelectId(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.id from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + // If we use select items directly, we only use the entity name on which the attribute was declared, + // so we can cut down the joined tables further + assertEquals( + "select " + + "t1_0.id " + + "from Thing t1_0 " + + "join House t1_2 on t1_0.id=t1_2.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "end=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) <> House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + // We need to join all tables because the EntityDomainResult will create fetches for all subtypes + // But actually, to know if a row is of type "House" or not, we need to join that table anyway + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "left join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end!=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "left join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end in (2,5)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeCommonSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Skyscraper)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end in (2,3)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) not in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "left join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end not in (2,5)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPath(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as House).familyName is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + // We need to join all tables because the EntityResult will create fetches for all subtypes. + // See #testEqTypeRestriction() for further explanation + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "join Building t1_1 on t1_0.id=t1_1.id " + + "join House t1_2 on t1_0.id=t1_2.id " + + "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "t1_2.familyName is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathSharedColumn(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as Skyscraper).doors is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end," + + "t1_6.seats," + + "t1_1.nr," + + "t1_5.doors," + + "t1_2.familyName," + + "t1_3.architectName," + + "t1_3.doors," + + "t1_4.name " + + "from Thing t1_0 " + + "join Building t1_1 on t1_0.id=t1_1.id " + + "left join House t1_2 on t1_0.id=t1_2.id " + + "join Skyscraper t1_3 on t1_0.id=t1_3.id " + + "left join Vehicle t1_4 on t1_0.id=t1_4.id " + + "left join Car t1_5 on t1_0.id=t1_5.id " + + "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "where " + + "case when case " + + "when t1_2.id is not null then 2 " + + "when t1_3.id is not null then 3 " + + "when t1_5.id is not null then 5 " + + "when t1_6.id is not null then 6 " + + "when t1_1.id is not null then 1 " + + "when t1_4.id is not null then 4 " + + "end=3 then t1_3.doors end is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testQueryChildUseParent(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.nr from Skyscraper t" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "s1_1.nr " + + "from Skyscraper s1_0 " + + "join Building s1_1 on s1_0.id=s1_1.id", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Entity(name = "Thing") + @Inheritance(strategy = InheritanceType.JOINED) + public static abstract class Thing { + @Id + private Long id; + + public Thing() { + } + + public Thing(Long id) { + this.id = id; + } + } + + @Entity(name = "Building") + public static class Building extends Thing { + + private Integer nr; + + public Building() { + } + } + + @Entity(name = "House") + public static class House extends Building { + + private String familyName; + + public House() { + } + } + + @Entity(name = "Skyscraper") + public static class Skyscraper extends Building { + + private String architectName; + private Integer doors; + + public Skyscraper() { + } + } + + @Entity(name = "Vehicle") + public static class Vehicle extends Thing { + + private String name; + + public Vehicle() { + } + } + + @Entity(name = "Car") + public static class Car extends Vehicle { + + private Integer doors; + + public Car() { + } + } + + @Entity(name = "Airplane") + public static class Airplane extends Vehicle { + + private Integer seats; + + public Airplane() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java new file mode 100644 index 0000000000..a9cabb2557 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java @@ -0,0 +1,423 @@ +/* + * 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.jpa.criteria; + +import org.hibernate.dialect.H2Dialect; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + EntityUseSingleTableOptimizationTest.Thing.class, + EntityUseSingleTableOptimizationTest.Building.class, + EntityUseSingleTableOptimizationTest.House.class, + EntityUseSingleTableOptimizationTest.Skyscraper.class, + EntityUseSingleTableOptimizationTest.Vehicle.class, + EntityUseSingleTableOptimizationTest.Car.class, + EntityUseSingleTableOptimizationTest.Airplane.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +// Run only on H2 to avoid dealing with SQL dialect differences +@RequiresDialect( H2Dialect.class ) +public class EntityUseSingleTableOptimizationTest { + + @Test + public void testEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE='House'", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select 1 from Thing t where type(t) = Building" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "1 " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE='Building'", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqTypeRestrictionSelectId(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.id from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE='House'", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) <> House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE!='House'", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE in ('House','Car')", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeCommonSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Skyscraper)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE in ('House','Skyscraper')", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) not in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE not in ('House','Car')", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPath(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as House).familyName is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (select * from Thing t where t.DTYPE='House') t1_0 " + + "where " + + "t1_0.familyName is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathSharedColumn(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as Skyscraper).doors is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (select * from Thing t where t.DTYPE='Skyscraper') t1_0 " + + "where " + + "case when t1_0.DTYPE='Skyscraper' then t1_0.doors end is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathInDisjunction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as House).familyName is not null or t.id > 0" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.familyName is not null or t1_0.id>0", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTypeRestrictionInDisjunction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) = House or t.id > 0" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.DTYPE," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from Thing t1_0 " + + "where " + + "t1_0.DTYPE='House' or t1_0.id>0", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testQueryChildUseParent(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.nr from Skyscraper t" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "s1_0.nr " + + "from Thing s1_0 " + + "where s1_0.DTYPE='Skyscraper'", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Entity(name = "Thing") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static abstract class Thing { + @Id + private Long id; + + public Thing() { + } + + public Thing(Long id) { + this.id = id; + } + } + + @Entity(name = "Building") + public static class Building extends Thing { + + private Integer nr; + + public Building() { + } + } + + @Entity(name = "House") + public static class House extends Building { + + private String familyName; + + public House() { + } + } + + @Entity(name = "Skyscraper") + public static class Skyscraper extends Building { + + private String architectName; + private Integer doors; + + public Skyscraper() { + } + } + + @Entity(name = "Vehicle") + public static class Vehicle extends Thing { + + private String name; + + public Vehicle() { + } + } + + @Entity(name = "Car") + public static class Car extends Vehicle { + + private Integer doors; + + public Car() { + } + } + + @Entity(name = "Airplane") + public static class Airplane extends Vehicle { + + private Integer seats; + + public Airplane() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java new file mode 100644 index 0000000000..10b409b4ba --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java @@ -0,0 +1,402 @@ +/* + * 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.jpa.criteria; + +import org.hibernate.dialect.H2Dialect; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + EntityUseUnionSubclassOptimizationTest.Thing.class, + EntityUseUnionSubclassOptimizationTest.Building.class, + EntityUseUnionSubclassOptimizationTest.House.class, + EntityUseUnionSubclassOptimizationTest.Skyscraper.class, + EntityUseUnionSubclassOptimizationTest.Vehicle.class, + EntityUseUnionSubclassOptimizationTest.Car.class, + EntityUseUnionSubclassOptimizationTest.Airplane.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +// Run only on H2 to avoid dealing with SQL dialect differences +@RequiresDialect( H2Dialect.class ) +public class EntityUseUnionSubclassOptimizationTest { + + @Test + public void testEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + ") t1_0 " + + "where " + + "t1_0.clazz_=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select 1 from Thing t where type(t) = Building" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "1 " + + "from (" + + "select id, nr, null as familyName, null as architectName, null as doors, 1 as clazz_ from Building" + + ") t1_0 " + + "where " + + "t1_0.clazz_=1", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testEqTypeRestrictionSelectId(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.id from Thing t where type(t) = House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id " + + "from (" + + "select id, nr, familyName, 2 as clazz_ from House" + + ") t1_0 " + + "where " + + "t1_0.clazz_=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotEqTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) <> House" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "union all " + + "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "union all " + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + ") t1_0 " + + "where " + + "t1_0.clazz_!=2", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "union all " + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + ") t1_0 " + + "where " + + "t1_0.clazz_ in (2,5)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testInTypeCommonSuperTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) in (House, Skyscraper)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House " + + "union all " + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper" + + ") t1_0 " + + "where " + + "t1_0.clazz_ in (2,3)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testNotInTypeRestriction(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where type(t) not in (House, Car)" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "union all " + + "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "union all " + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + ") t1_0 " + + "where " + + "t1_0.clazz_ not in (2,5)", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPath(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as House).familyName is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + ") t1_0 " + + "where " + + "t1_0.familyName is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathSharedColumn(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "from Thing t where treat(t as Skyscraper).doors is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.clazz_," + + "t1_0.seats," + + "t1_0.nr," + + "t1_0.doors," + + "t1_0.familyName," + + "t1_0.architectName," + + "t1_0.name " + + "from (" + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper" + + ") t1_0 " + + "where " + + "case when t1_0.clazz_=3 then t1_0.doors end is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testQueryChildUseParent(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select t.nr from Skyscraper t" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "s1_0.nr " + + "from Skyscraper s1_0", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Entity(name = "Thing") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public static abstract class Thing { + @Id + private Long id; + + public Thing() { + } + + public Thing(Long id) { + this.id = id; + } + } + + @Entity(name = "Building") + public static class Building extends Thing { + + private Integer nr; + + public Building() { + } + } + + @Entity(name = "House") + public static class House extends Building { + + private String familyName; + + public House() { + } + } + + @Entity(name = "Skyscraper") + public static class Skyscraper extends Building { + + private String architectName; + private Integer doors; + + public Skyscraper() { + } + } + + @Entity(name = "Vehicle") + public static class Vehicle extends Thing { + + private String name; + + public Vehicle() { + } + } + + @Entity(name = "Car") + public static class Car extends Vehicle { + + private Integer doors; + + public Car() { + } + } + + @Entity(name = "Airplane") + public static class Airplane extends Vehicle { + + private Integer seats; + + public Airplane() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/TreatDisjunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/TreatDisjunctionTest.java new file mode 100644 index 0000000000..bfc0029640 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/TreatDisjunctionTest.java @@ -0,0 +1,140 @@ +/* + * 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.jpa.criteria; + +import java.util.List; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + TreatDisjunctionTest.PAccountDirectory.class, + TreatDisjunctionTest.PLDAPDirectory.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +public class TreatDisjunctionTest { + + @Test + @TestForIssue( jiraKey = "HHH-15726") + public void testQuery(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + PAccountDirectory base = new PAccountDirectory(); + base.setActive( true ); + entityManager.persist( base ); + + PLDAPDirectory sub = new PLDAPDirectory(); + sub.setActive( false ); + sub.setOpenldap( true ); + entityManager.persist( sub ); + entityManager.flush(); + entityManager.clear(); + + sqlStatementInterceptor.clear(); + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( PAccountDirectory.class ); + Root root = query.from( PAccountDirectory.class ); + + From ldap = cb.treat( root, PLDAPDirectory.class ); + Predicate exp = cb.or( + cb.equal( root.get( "active" ), true ), + cb.equal( ldap.get( "openldap" ), true ) + ); + + List directories = entityManager.createQuery( query.select( root ).where( exp ) ) + .getResultList(); + assertThat( directories, hasSize( 2 ) ); + + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "p1_0.id," + + "p1_0.DTYPE," + + "p1_0.active," + + "p1_0.openldap " + + "from PAccountDirectory p1_0 " + + "where p1_0.active=? " + + "or p1_0.openldap=?", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @MappedSuperclass + public static abstract class BaseEntity { + @Id + @GeneratedValue + private Long id; + + public BaseEntity() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } + + @Entity(name = "PAccountDirectory") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static class PAccountDirectory extends BaseEntity { + @Column(nullable = false) + private boolean active; + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + } + + @Entity(name = "PLDAPDirectory") + public static class PLDAPDirectory extends PAccountDirectory { + @Column(nullable = true) + private boolean openldap; + + public boolean isOpenldap() { + return openldap; + } + + public void setOpenldap(boolean openldap) { + this.openldap = openldap; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/AbstractPathImplTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/AbstractPathImplTest.java index c2431c6b6b..4865b8b866 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/AbstractPathImplTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/AbstractPathImplTest.java @@ -113,10 +113,10 @@ public class AbstractPathImplTest extends AbstractMetamodelSpecificTest { Root thingRoot = criteria.from( Thing.class ); criteria.select( thingRoot ); - assertEquals( em.createQuery( criteria ).getResultList().size(), 3); + assertEquals( 3, em.createQuery( criteria ).getResultList().size() ); criteria.where( criteriaBuilder.equal( thingRoot.type(), criteriaBuilder.literal( Thing.class ) ) ); - assertEquals( em.createQuery( criteria ).getResultList().size(), 2 ); + assertEquals( 2, em.createQuery( criteria ).getResultList().size() ); } finally { em.close(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedSubclassDuplicateFieldsWithTreatTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedSubclassDuplicateFieldsWithTreatTest.java index 8a840d378c..5ff6358ae0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedSubclassDuplicateFieldsWithTreatTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedSubclassDuplicateFieldsWithTreatTest.java @@ -13,7 +13,6 @@ import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; -import org.hibernate.testing.orm.junit.FailureExpected; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; @@ -37,7 +36,6 @@ import static org.assertj.core.api.Assertions.assertThat; public class JoinedSubclassDuplicateFieldsWithTreatTest { @Test - @FailureExpected( jiraKey = "HHH-11686" ) public void queryConstrainedSubclass(SessionFactoryScope scope) { scope.inTransaction( (session) -> { Deposit deposit1 = new Deposit();