HHH-15726 Fix treat disjunction handling and improve pushdown

This commit is contained in:
Christian Beikov 2023-05-15 10:09:56 +02:00
parent 167a14bcc7
commit 5f99dd3862
35 changed files with 2848 additions and 384 deletions

View File

@ -124,7 +124,7 @@ public class SQLServerLegacySqlAstTranslator<T extends JdbcOperation> 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( " )" );

View File

@ -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;
}
}

View File

@ -123,7 +123,7 @@ public class SQLServerSqlAstTranslator<T extends JdbcOperation> 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( " )" );

View File

@ -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<String, String>[] filterAliasTableMaps;
private final List<String>[] parameterNames;
private final Map<String, String> tableToEntityName;
public FilterHelper(List<FilterConfiguration> 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<FilterConfiguration> filters, SessionFactoryImplementor factory) {
public FilterHelper(List<FilterConfiguration> filters, Map<String, String> 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<Predicate> predicateConsumer,
FilterAliasGenerator aliasGenerator,
Map<String, Filter> enabledFilters) {
final FilterPredicate predicate = generateFilterPredicate( aliasGenerator, enabledFilters );
Map<String, Filter> 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<String, Filter> enabledFilters) {
private FilterPredicate generateFilterPredicate(
FilterAliasGenerator aliasGenerator,
Map<String, Filter> 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<String, String> 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<String, String> 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;
}

View File

@ -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<TableGroup, Map<EntityDomainType<?>, Boolean>> getTreatRegistrations() {
return Collections.emptyMap();
}
@Override
public SqlExpressionResolver getSqlExpressionResolver() {
return processingState;

View File

@ -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<CheckConstraint> 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;
}
}

View File

@ -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.
* <p>
* The goal is to e.g. remove join inheritance "branches" or union selects that are impossible.
* <p>
* Consider the following example:
* <code>
* 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 {}
* </code>
* <p>
* If the <code>treatedEntityNames</code> only contains <code>Sub1</code> or any of its subtypes,
* this means that <code>Sub2</code> 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<String, EntityNameUse> 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<String> treatedEntityNames) {
}

View File

@ -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<J>
private volatile Map<String, PluralPersistentAttribute<J, ?, ?>> declaredPluralAttributes ;
private volatile Map<String, PersistentAttribute<J, ?>> declaredConcreteGenericAttributes;
private final List<ManagedDomainType> subTypes = new ArrayList<>();
private final List<ManagedDomainType<? extends J>> subTypes = new ArrayList<>();
protected AbstractManagedType(
String hibernateTypeName,
@ -87,6 +88,11 @@ public abstract class AbstractManagedType<J>
return superType;
}
@Override
public Collection<? extends ManagedDomainType<? extends J>> getSubTypes() {
return subTypes;
}
@Override
public void addSubType(ManagedDomainType subType){
subTypes.add( subType );

View File

@ -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<J> extends IdentifiableDomainType<J>, EntityType<J>, SqmPathSource<J> {
String getHibernateEntityName();
@Override
Collection<? extends EntityDomainType<? extends J>> getSubTypes();
}

View File

@ -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<J> extends SqmExpressible<J>, DomainType<J>,
*/
ManagedDomainType<? super J> getSuperType();
Collection<? extends ManagedDomainType<? extends J>> getSubTypes();
void addSubType(ManagedDomainType subType);
void visitAttributes(Consumer<? super PersistentAttribute<J, ?>> action);

View File

@ -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<J>
return super.getSuperType();
}
@Override
public Collection<? extends EntityDomainType<? extends J>> getSubTypes() {
//noinspection unchecked
return (Collection<? extends EntityDomainType<? extends J>>) super.getSubTypes();
}
@Override
@SuppressWarnings("unchecked")
public <S extends J> SubGraphImplementor<S> makeSubGraph(Class<S> subType) {

View File

@ -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<String, String> 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<Predicate> predicateConsumer, TableGroup tableGroup, boolean useQualifier, Map<String, Filter> enabledFilters, SqlAstCreationState creationState) {
public void applyFilterRestrictions(
Consumer<Predicate> predicateConsumer,
TableGroup tableGroup,
boolean useQualifier,
Map<String, Filter> 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<Predicate> predicateConsumer, TableGroup tableGroup, boolean useQualifier, Map<String, Filter> enabledFilters, Set<String> treatAsDeclarations, SqlAstCreationState creationState) {
public void applyBaseManyToManyRestrictions(
Consumer<Predicate> predicateConsumer,
TableGroup tableGroup,
boolean useQualifier,
Map<String, Filter> enabledFilters,
Set<String> 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 ) {

View File

@ -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<String> sharedColumnNames;
private final List<Integer> 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<Property> thisClassProperties = new HashSet<>();
final ArrayList<String> 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<Object> values = new ArrayList<>();
// final List<String> sqlValues = new ArrayList<>();
final List<String> 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<Subclass> 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<String, String> getEntityNameByTableNameMap(PersistentClass persistentClass) {
final Map<String, String> 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<Object> 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
);
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}

View File

@ -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<String> treatedEntityNames) {
final Set<TableReference> retainedTableReferences = new HashSet<>( treatedEntityNames.size() );
final Set<String> sharedSuperclassTables = new HashSet<>();
public void pruneForSubclasses(TableGroup tableGroup, Map<String, EntityNameUse> entityNameUses) {
final Set<TableReference> 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<TableReferenceJoin> 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<String> tablesToInnerJoin = innerJoinOptimization ? new HashSet<>() : null;
for ( Map.Entry<String, EntityNameUse> 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<TableReferenceJoin> 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 {

View File

@ -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<String> treatedEntityNames) {
if ( !needsDiscriminator() && treatedEntityNames.isEmpty() ) {
public void pruneForSubclasses(TableGroup tableGroup, Map<String, EntityNameUse> 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<String, EntityNameUse> 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<String, EntityNameUse> 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<String> 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<String> 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

View File

@ -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<String> treatedEntityNames) {
public void pruneForSubclasses(TableGroup tableGroup, Map<String, EntityNameUse> 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<PersistentClass> classes = new JoinedList<>(
final List<PersistentClass> 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<String> treated) {
protected String generateSubquery(Map<String, EntityNameUse> 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<String, Map<String, SelectableMapping>> selectables = new LinkedHashMap<>();
// Collect the concrete subclass table names for the treated entity names
final Set<String> 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<String> 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<String, EntityNameUse> 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<String, EntityNameUse> 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<EntityMappingType> subMappingTypes = getSubMappingTypes();
final ArrayList<EntityMappingType> 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<String, Map<String, SelectableMapping>> 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 );
}
}
}
}

View File

@ -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 );

View File

@ -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<T> extends AbstractSqmPathInterpretat
public static <T> BasicValuedPathInterpretation<T> from(
SqmBasicValuedSimplePath<T> 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<T> 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<T> extends AbstractSqmPathInterpretat
return columnReference;
}
@Override
public ColumnReference getColumnReference() {
return columnReference;
}
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
columnReference.accept( sqlTreeWalker );

View File

@ -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<TableGroup, Map<EntityDomainType<?>, 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<EntityDomainType<?>, 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<TableGroup, Map<EntityDomainType<?>, Boolean>> getTreatRegistrations() {
return treatRegistrations;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SqlExpressionResolver

View File

@ -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<T> implements EntityDomainType<T> {
throw new UnsupportedOperationException( );
}
@Override
public Collection<? extends EntityDomainType<? extends T>> getSubTypes() {
throw new UnsupportedOperationException( );
}
@Override
public void addSubType(ManagedDomainType subType) {
throw new UnsupportedOperationException( );

View File

@ -57,6 +57,10 @@ public class InFragment {
return setColumn( this.columnName );
}
public List<Object> getValues() {
return values;
}
public String toFragmentString() {
if ( values.size() == 0 ) {
return "1=2";

View File

@ -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
}
}

View File

@ -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<TableGroup, Map<EntityDomainType<?>, Boolean>> getTreatRegistrations();
}

View File

@ -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<Predicate> getJoinPredicateConsumer() {
return joinPredicateConsumer;
}
public TableGroup getCorrelatedTableGroup(){
return correlatedTableGroup;
}
}

View File

@ -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 );
}
}

View File

@ -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<String> subclassEntityNamesLocal = new HashSet<>();
// Need deterministic ordering
final Set<String> subclassEntityNamesLocal = new LinkedHashSet<>();
subclassEntityNamesLocal.add( name );
for ( Subclass subclass : persistentClass.getSubclasses() ) {
subclassEntityNamesLocal.add( subclass.getEntityName() );
}
subclassEntityNamesLocal.add( name );
subclassEntityNames = toSmallSet( subclassEntityNamesLocal );
HashMap<Class<?>, String> entityNameByInheritanceClassMapLocal = new HashMap<>();

View File

@ -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 -> {

View File

@ -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() {
}
}
}

View File

@ -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() {
}
}
}

View File

@ -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() {
}
}
}

View File

@ -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<PAccountDirectory> query = cb.createQuery( PAccountDirectory.class );
Root<PAccountDirectory> root = query.from( PAccountDirectory.class );
From<?, PLDAPDirectory> ldap = cb.treat( root, PLDAPDirectory.class );
Predicate exp = cb.or(
cb.equal( root.get( "active" ), true ),
cb.equal( ldap.get( "openldap" ), true )
);
List<PAccountDirectory> 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;
}
}
}

View File

@ -113,10 +113,10 @@ public class AbstractPathImplTest extends AbstractMetamodelSpecificTest {
Root<Thing> 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();

View File

@ -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();