HHH-16773 Introduce support for group/order by PK functional dependency

This commit is contained in:
Marco Belladelli 2023-06-12 10:20:30 +02:00
parent 51460470f4
commit e5d59b64fd
17 changed files with 523 additions and 148 deletions

View File

@ -19,26 +19,13 @@ import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.persistence.TemporalType;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.PessimisticLockException;
import org.hibernate.QueryTimeoutException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.NationalizationSupport;
import org.hibernate.dialect.PgJdbcHelper;
import org.hibernate.dialect.PostgreSQLCastingInetJdbcType;
import org.hibernate.dialect.PostgreSQLCastingIntervalSecondJdbcType;
import org.hibernate.dialect.PostgreSQLCastingJsonJdbcType;
import org.hibernate.dialect.PostgreSQLDriverKind;
import org.hibernate.dialect.RowLockStrategy;
import org.hibernate.dialect.SimpleDatabaseVersion;
import org.hibernate.dialect.SpannerDialect;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.FormatFunction;
import org.hibernate.dialect.function.PostgreSQLTruncFunction;
@ -87,11 +74,38 @@ import org.hibernate.type.spi.TypeConfiguration;
import org.jboss.logging.Logger;
import jakarta.persistence.TemporalType;
import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate;
import static org.hibernate.query.sqm.TemporalUnit.DAY;
import static org.hibernate.query.sqm.TemporalUnit.EPOCH;
import static org.hibernate.query.sqm.TemporalUnit.NATIVE;
import static org.hibernate.type.SqlTypes.*;
import static org.hibernate.type.SqlTypes.ARRAY;
import static org.hibernate.type.SqlTypes.BINARY;
import static org.hibernate.type.SqlTypes.BLOB;
import static org.hibernate.type.SqlTypes.CHAR;
import static org.hibernate.type.SqlTypes.CLOB;
import static org.hibernate.type.SqlTypes.GEOGRAPHY;
import static org.hibernate.type.SqlTypes.GEOMETRY;
import static org.hibernate.type.SqlTypes.INET;
import static org.hibernate.type.SqlTypes.INTEGER;
import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.LONG32NVARCHAR;
import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
import static org.hibernate.type.SqlTypes.NCHAR;
import static org.hibernate.type.SqlTypes.NCLOB;
import static org.hibernate.type.SqlTypes.NVARCHAR;
import static org.hibernate.type.SqlTypes.OTHER;
import static org.hibernate.type.SqlTypes.TIME;
import static org.hibernate.type.SqlTypes.TIMESTAMP;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.TIME_UTC;
import static org.hibernate.type.SqlTypes.TINYINT;
import static org.hibernate.type.SqlTypes.UUID;
import static org.hibernate.type.SqlTypes.VARBINARY;
import static org.hibernate.type.SqlTypes.VARCHAR;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime;
@ -1022,6 +1036,11 @@ public class CockroachLegacyDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public RowLockStrategy getWriteRowLockStrategy() {
return getVersion().isSameOrAfter( 20, 1 ) ? RowLockStrategy.TABLE : RowLockStrategy.NONE;

View File

@ -19,14 +19,7 @@ import java.util.TimeZone;
import org.hibernate.PessimisticLockException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.H2DurationIntervalSecondJdbcType;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.Replacer;
import org.hibernate.dialect.SelectItemReferenceStrategy;
import org.hibernate.dialect.SimpleDatabaseVersion;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.hint.IndexQueryHintHandler;
import org.hibernate.dialect.identity.H2FinalTableIdentityColumnSupport;
@ -829,6 +822,11 @@ public class H2LegacyDialect extends Dialect {
return getVersion().isSameOrAfter( 1, 4, 198 );
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public IdentityColumnSupport getIdentityColumnSupport() {
return getVersion().isSameOrAfter( 2 ) ? H2FinalTableIdentityColumnSupport.INSTANCE : H2IdentityColumnSupport.INSTANCE;

View File

@ -14,11 +14,7 @@ import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.StaleObjectStateException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.dialect.BooleanDecoder;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.SimpleDatabaseVersion;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.identity.HSQLIdentityColumnSupport;
import org.hibernate.dialect.identity.IdentityColumnSupport;
@ -786,6 +782,11 @@ public class HSQLLegacyDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_REFERENCE;
}
// Do not drop constraints explicitly, just do this by cascading instead.
@Override
public boolean dropConstraints() {

View File

@ -11,13 +11,7 @@ import java.sql.SQLException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.InnoDBStorageEngine;
import org.hibernate.dialect.MySQLServerConfiguration;
import org.hibernate.dialect.MySQLStorageEngine;
import org.hibernate.dialect.NationalizationSupport;
import org.hibernate.dialect.VarcharUUIDJdbcType;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.sequence.MariaDBSequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
@ -249,6 +243,11 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData)
throws SQLException {

View File

@ -17,16 +17,7 @@ import org.hibernate.PessimisticLockException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.InnoDBStorageEngine;
import org.hibernate.dialect.MyISAMStorageEngine;
import org.hibernate.dialect.MySQLCastingJsonJdbcType;
import org.hibernate.dialect.MySQLServerConfiguration;
import org.hibernate.dialect.MySQLStorageEngine;
import org.hibernate.dialect.Replacer;
import org.hibernate.dialect.RowLockStrategy;
import org.hibernate.dialect.SelectItemReferenceStrategy;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.hint.IndexQueryHintHandler;
import org.hibernate.dialect.identity.IdentityColumnSupport;
@ -1367,6 +1358,11 @@ public class MySQLLegacyDialect extends Dialect {
return getMySQLVersion().isSameOrAfter( 8 );
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP;
}
@Override
public boolean canDisableConstraints() {
return true;

View File

@ -26,20 +26,7 @@ import org.hibernate.QueryTimeoutException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.community.dialect.sequence.PostgreSQLLegacySequenceSupport;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.NationalizationSupport;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.PgJdbcHelper;
import org.hibernate.dialect.PostgreSQLCastingInetJdbcType;
import org.hibernate.dialect.PostgreSQLCastingIntervalSecondJdbcType;
import org.hibernate.dialect.PostgreSQLCastingJsonJdbcType;
import org.hibernate.dialect.PostgreSQLDriverKind;
import org.hibernate.dialect.PostgreSQLStructCastingJdbcType;
import org.hibernate.dialect.Replacer;
import org.hibernate.dialect.RowLockStrategy;
import org.hibernate.dialect.SelectItemReferenceStrategy;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.dialect.*;
import org.hibernate.dialect.aggregate.AggregateSupport;
import org.hibernate.dialect.aggregate.PostgreSQLAggregateSupport;
import org.hibernate.dialect.function.CommonFunctionFactory;
@ -1287,6 +1274,11 @@ public class PostgreSQLLegacyDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_REFERENCE;
}
@Override
public RowLockStrategy getWriteRowLockStrategy() {
return RowLockStrategy.TABLE;

View File

@ -988,6 +988,11 @@ public class CockroachDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public RowLockStrategy getWriteRowLockStrategy() {
return RowLockStrategy.TABLE;

View File

@ -5299,4 +5299,12 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() {
return DmlTargetColumnQualifierSupport.NONE;
}
/**
* Get this dialect's level of support for primary key functional dependency analysis
* within {@code GROUP BY} and {@code ORDER BY} clauses.
*/
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.NONE;
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.dialect;
/**
* Dialect support information for primary key functional dependency analysis
* within {@code GROUP BY} and {@code ORDER BY} clauses.
*
* @author Marco Belladelli
*/
public interface FunctionalDependencyAnalysisSupport {
/**
* Supports primary key functional dependency analysis
*/
boolean supportsAnalysis();
/**
* Supports functional dependency analysis through joined tables and result sets (e.g. unions)
*/
boolean supportsTableGroups();
/**
* Also supports functional dependency analysis for constant values other than table columns
*/
boolean supportsConstants();
}

View File

@ -0,0 +1,78 @@
/*
* 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.dialect;
/**
* @author Marco Belladelli
*/
public class FunctionalDependencyAnalysisSupportImpl implements FunctionalDependencyAnalysisSupport {
private final boolean supportsAnalysis;
private final boolean supportsTableGroups;
private final boolean supportsConstants;
/**
* No support for functional dependency analysis
*/
public static final FunctionalDependencyAnalysisSupportImpl NONE = new FunctionalDependencyAnalysisSupportImpl(
false,
false,
false
);
/**
* Only supports the analysis for a single table reference, i.e. no support for joins / unions
*/
public static final FunctionalDependencyAnalysisSupportImpl TABLE_REFERENCE = new FunctionalDependencyAnalysisSupportImpl(
true,
false,
false
);
/**
* Supports the analysis for single tables, a group of joined tables or a result set (e.g. union)
* as long as only table columns are selected, i.e. no constants (see {@link #TABLE_GROUP_AND_CONSTANTS})
*/
public static final FunctionalDependencyAnalysisSupportImpl TABLE_GROUP = new FunctionalDependencyAnalysisSupportImpl(
true,
true,
false
);
/**
* Fully supports the analysis for joined / union table groups, including any constant value
* (e.g. the literal {@code clazz_} column used as table per class inheritance discriminator column)
*/
public static final FunctionalDependencyAnalysisSupportImpl TABLE_GROUP_AND_CONSTANTS = new FunctionalDependencyAnalysisSupportImpl(
true,
true,
true
);
public FunctionalDependencyAnalysisSupportImpl(
boolean supportsAnalysis,
boolean supportsTableGroups,
boolean supportsConstants) {
this.supportsAnalysis = supportsAnalysis;
this.supportsTableGroups = supportsTableGroups;
this.supportsConstants = supportsConstants;
}
@Override
public boolean supportsAnalysis() {
return supportsAnalysis;
}
@Override
public boolean supportsTableGroups() {
return supportsTableGroups;
}
@Override
public boolean supportsConstants() {
return supportsConstants;
}
}

View File

@ -69,11 +69,11 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.TimeAsTimestampWithTimeZoneJdbcType;
import org.hibernate.type.descriptor.jdbc.TimeUtcAsJdbcTimeJdbcType;
import org.hibernate.type.descriptor.jdbc.TimestampUtcAsInstantJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType;
import org.hibernate.type.descriptor.jdbc.TimestampUtcAsInstantJdbcType;
import org.hibernate.type.descriptor.jdbc.UUIDJdbcType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
@ -823,6 +823,11 @@ public class H2Dialect extends Dialect {
return getVersion().isSameOrAfter( 1, 4, 198 );
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public IdentityColumnSupport getIdentityColumnSupport() {
return getVersion().isSameOrAfter( 2 )

View File

@ -605,6 +605,11 @@ public class HSQLDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_REFERENCE;
}
// Do not drop constraints explicitly, just do this by cascading instead.
@Override
public boolean dropConstraints() {

View File

@ -248,6 +248,11 @@ public class MariaDBDialect extends MySQLDialect {
return getVersion().isSameOrAfter( 10, 5 );
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP_AND_CONSTANTS;
}
@Override
public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData)
throws SQLException {

View File

@ -1470,6 +1470,11 @@ public class MySQLDialect extends Dialect {
return getMySQLVersion().isSameOrAfter( 8 );
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP;
}
@Override
public boolean canDisableConstraints() {
return true;

View File

@ -78,7 +78,6 @@ import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation;
import org.hibernate.type.JavaObjectType;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.AggregateJdbcType;
import org.hibernate.type.descriptor.jdbc.ArrayJdbcType;
import org.hibernate.type.descriptor.jdbc.BlobJdbcType;
import org.hibernate.type.descriptor.jdbc.ClobJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
@ -99,7 +98,34 @@ import jakarta.persistence.TemporalType;
import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate;
import static org.hibernate.query.sqm.TemporalUnit.DAY;
import static org.hibernate.query.sqm.TemporalUnit.EPOCH;
import static org.hibernate.type.SqlTypes.*;
import static org.hibernate.type.SqlTypes.ARRAY;
import static org.hibernate.type.SqlTypes.BINARY;
import static org.hibernate.type.SqlTypes.BLOB;
import static org.hibernate.type.SqlTypes.CHAR;
import static org.hibernate.type.SqlTypes.CLOB;
import static org.hibernate.type.SqlTypes.FLOAT;
import static org.hibernate.type.SqlTypes.GEOGRAPHY;
import static org.hibernate.type.SqlTypes.GEOMETRY;
import static org.hibernate.type.SqlTypes.INET;
import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.LONG32NVARCHAR;
import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
import static org.hibernate.type.SqlTypes.NCHAR;
import static org.hibernate.type.SqlTypes.NCLOB;
import static org.hibernate.type.SqlTypes.NVARCHAR;
import static org.hibernate.type.SqlTypes.OTHER;
import static org.hibernate.type.SqlTypes.SQLXML;
import static org.hibernate.type.SqlTypes.STRUCT;
import static org.hibernate.type.SqlTypes.TIME;
import static org.hibernate.type.SqlTypes.TIMESTAMP;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.TIME_UTC;
import static org.hibernate.type.SqlTypes.TINYINT;
import static org.hibernate.type.SqlTypes.UUID;
import static org.hibernate.type.SqlTypes.VARBINARY;
import static org.hibernate.type.SqlTypes.VARCHAR;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime;
import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime;
@ -1308,6 +1334,11 @@ public class PostgreSQLDialect extends Dialect {
return false;
}
@Override
public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() {
return FunctionalDependencyAnalysisSupportImpl.TABLE_REFERENCE;
}
@Override
public RowLockStrategy getWriteRowLockStrategy() {
return RowLockStrategy.TABLE;

View File

@ -11,6 +11,8 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.FunctionalDependencyAnalysisSupport;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping;
@ -23,6 +25,7 @@ import org.hibernate.metamodel.mapping.SelectableConsumer;
import org.hibernate.metamodel.mapping.ValuedModelPart;
import org.hibernate.metamodel.mapping.internal.EntityCollectionPart;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.persister.entity.UnionSubclassEntityPersister;
import org.hibernate.query.derived.AnonymousTupleEntityValuedModelPart;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
@ -49,6 +52,7 @@ import org.hibernate.sql.results.graph.Fetchable;
public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpretation<T>
implements SqlTupleContainer, Assignable {
private final Expression sqlExpression;
public static <T> EntityValuedPathInterpretation<T> from(
SqmEntityValuedSimplePath<T> sqmPath,
@ -266,7 +270,7 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
}
else {
// When the table group is selected and the navigablePath is selected we need to expand
// to all columns, as we also expand this to all columns in the select clause
// to all columns, as we must make sure we include all columns present in the select clause
expandToAllColumns = isSelected( tableGroup, navigablePath, querySpec );
}
}
@ -277,33 +281,38 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
final SqlExpressionResolver sqlExprResolver = sqlAstCreationState.getSqlExpressionResolver();
final Expression sqlExpression;
if ( expandToAllColumns ) {
// Expand to all columns of the entity mapping type, as we already did for the selection
// Expand to all columns of the entity mapping type to ensure a correct group / order by expression,
// or use only the primary key if the dialect supports functional dependency
final Dialect dialect = sqlAstCreationState.getCreationContext()
.getSessionFactory()
.getJdbcServices()
.getDialect();
final EntityMappingType entityMappingType = mapping.getEntityMappingType();
final EntityIdentifierMapping identifierMapping = entityMappingType.getIdentifierMapping();
final EntityDiscriminatorMapping discriminatorMapping = entityMappingType.getDiscriminatorMapping();
final int numberOfFetchables = entityMappingType.getNumberOfFetchables();
final List<Expression> expressions = new ArrayList<>(
numberOfFetchables + identifierMapping.getJdbcTypeCount()
+ ( discriminatorMapping == null ? 0 : 1 )
);
final TableGroup parentTableGroup = tableGroup;
final List<Expression> expressions = new ArrayList<>( identifierMapping.getJdbcTypeCount() );
final SelectableConsumer selectableConsumer = (selectionIndex, selectableMapping) -> {
final TableReference tableReference = parentTableGroup.resolveTableReference(
final TableReference tableReference = tableGroup.resolveTableReference(
navigablePath,
selectableMapping.getContainingTableExpression()
);
expressions.add(
sqlExprResolver.resolveSqlExpression( tableReference, selectableMapping )
);
expressions.add( sqlExprResolver.resolveSqlExpression( tableReference, selectableMapping ) );
};
identifierMapping.forEachSelectable( selectableConsumer );
if ( discriminatorMapping != null ) {
discriminatorMapping.forEachSelectable( selectableConsumer );
}
for ( int i = 0; i < numberOfFetchables; i++ ) {
final Fetchable fetchable = entityMappingType.getFetchable( i );
if ( fetchable.isSelectable() ) {
fetchable.forEachSelectable( selectableConsumer );
if ( !supportsFunctionalDependency( dialect, entityMappingType ) ) {
final EntityDiscriminatorMapping discriminatorMapping = entityMappingType.getDiscriminatorMapping();
if ( discriminatorMapping != null ) {
expressions.add( discriminatorMapping.resolveSqlExpression(
navigablePath,
discriminatorMapping.getUnderlyingJdbcMapping(),
tableGroup,
sqlAstCreationState
) );
}
for ( int i = 0; i < entityMappingType.getNumberOfFetchables(); i++ ) {
final Fetchable fetchable = entityMappingType.getFetchable( i );
if ( fetchable.isSelectable() ) {
fetchable.forEachSelectable( selectableConsumer );
}
}
}
sqlExpression = new SqlTuple( expressions, entityMappingType );
@ -373,7 +382,21 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
return false;
}
private final Expression sqlExpression;
private static boolean supportsFunctionalDependency(Dialect dialect, EntityMappingType entityMappingType) {
final FunctionalDependencyAnalysisSupport analysisSupport = dialect.getFunctionalDependencyAnalysisSupport();
if ( analysisSupport.supportsAnalysis() ) {
if ( entityMappingType.getSqmMultiTableMutationStrategy() == null ) {
return true;
}
else {
return analysisSupport.supportsTableGroups() && ( analysisSupport.supportsConstants() ||
// Union entity persisters use a literal 'clazz_' column as a discriminator
// that breaks functional dependency for dialects that don't support constants
!( entityMappingType.getEntityPersister() instanceof UnionSubclassEntityPersister ) );
}
}
return false;
}
public EntityValuedPathInterpretation(
Expression sqlExpression,

View File

@ -6,6 +6,11 @@
*/
package org.hibernate.orm.test.inheritance;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.FunctionalDependencyAnalysisSupport;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.persister.entity.UnionSubclassEntityPersister;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
@ -22,8 +27,11 @@ import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import static org.assertj.core.api.Assertions.assertThat;
@ -33,19 +41,34 @@ import static org.assertj.core.api.Assertions.assertThat;
@SessionFactory( useCollectingStatementInspector = true )
@DomainModel( annotatedClasses = {
InheritanceQueryGroupByTest.Parent.class,
InheritanceQueryGroupByTest.ChildOne.class,
InheritanceQueryGroupByTest.ChildTwo.class,
InheritanceQueryGroupByTest.MyEntity.class
InheritanceQueryGroupByTest.SingleTableParent.class,
InheritanceQueryGroupByTest.SingleTableChildOne.class,
InheritanceQueryGroupByTest.SingleTableChildTwo.class,
InheritanceQueryGroupByTest.JoinedParent.class,
InheritanceQueryGroupByTest.JoinedChildOne.class,
InheritanceQueryGroupByTest.JoinedChildTwo.class,
InheritanceQueryGroupByTest.TPCParent.class,
InheritanceQueryGroupByTest.TPCChildOne.class,
InheritanceQueryGroupByTest.TPCChildTwo.class,
InheritanceQueryGroupByTest.MyEntity.class,
} )
@Jira( "https://hibernate.atlassian.net/browse/HHH-16349" )
@Jira( "https://hibernate.atlassian.net/browse/HHH-16773" )
public class InheritanceQueryGroupByTest {
@BeforeAll
public void setUp(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final ChildOne childOne = new ChildOne( "child_one", 1 );
session.persist( childOne );
session.persist( new MyEntity( 1, childOne ) );
session.persist( new MyEntity( 2, childOne ) );
final SingleTableChildOne st1 = new SingleTableChildOne();
st1.setName( "single_table_child_one" );
session.persist( st1 );
final JoinedChildOne j1 = new JoinedChildOne();
j1.setName( "joined_child_one" );
session.persist( j1 );
final TPCChildOne tpc1 = new TPCChildOne();
tpc1.setName( "tpc_child_one" );
session.persist( tpc1 );
session.persist( new MyEntity( 1, st1, j1, tpc1 ) );
session.persist( new MyEntity( 2, st1, j1, tpc1 ) );
} );
}
@ -53,82 +76,199 @@ public class InheritanceQueryGroupByTest {
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createMutationQuery( "delete from MyEntity" ).executeUpdate();
session.createMutationQuery( "delete from Parent" ).executeUpdate();
session.createMutationQuery( "delete from " + Parent.class.getName() ).executeUpdate();
} );
}
@Test
public void testGroupBy(SessionFactoryScope scope) {
public void testGroupBySingleTable(SessionFactoryScope scope) {
testGroupBy( scope, "singleTableParent", SingleTableParent.class, "single_table_child_one", 1 );
}
@Test
public void testGroupByJoined(SessionFactoryScope scope) {
testGroupBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 );
}
@Test
public void testGroupByTPC(SessionFactoryScope scope) {
testGroupBy( scope, "tpcParent", TPCParent.class, "tpc_child_one", 4 );
}
private void testGroupBy(
SessionFactoryScope scope,
String parentProp,
Class<?> parentEntityClass,
String parentName,
int childPropCount) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( session -> {
final MyPojo myPojo = session.createQuery(
"select new "
+ MyPojo.class.getName()
+ "(sum(e.amount), re) from MyEntity e join e.parent re group by re",
String.format(
"select new %s(sum(e.amount), re) from MyEntity e join e.%s re group by re",
MyPojo.class.getName(),
parentProp
),
MyPojo.class
).getSingleResult();
assertThat( myPojo.getAmount() ).isEqualTo( 3L );
assertThat( myPojo.getParent().getName() ).isEqualTo( "child_one" );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", 2 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", 2 );
assertThat( myPojo.getParent().getName() ).isEqualTo( parentName );
final EntityMappingType entityMappingType = scope.getSessionFactory()
.getMappingMetamodel()
.findEntityDescriptor( parentEntityClass );
final int expectedCount = supportsFunctionalDependency( scope, entityMappingType ) ?
childPropCount :
childPropCount + 1;
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", expectedCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", expectedCount );
} );
}
@Test
public void testGroupByNotSelected(SessionFactoryScope scope) {
public void testGroupByNotSelectedSingleTable(SessionFactoryScope scope) {
testGroupByNotSelected( scope, "singleTableParent", "single_table_parent_id", 0 );
}
@Test
public void testGroupByNotSelectedJoined(SessionFactoryScope scope) {
testGroupByNotSelected( scope, "joinedParent", "joined_parent_id", 0 );
}
@Test
public void testGroupByNotSelectedTPC(SessionFactoryScope scope) {
testGroupByNotSelected( scope, "tpcParent", "tpc_parent_id", 3 );
}
private void testGroupByNotSelected(
SessionFactoryScope scope,
String parentProp,
String parentFkName,
int childPropCount) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( session -> {
final Long sum = session.createQuery(
"select sum(e.amount) from MyEntity e join e.parent re group by re",
String.format( "select sum(e.amount) from MyEntity e join e.%s re group by re", parentProp ),
Long.class
).getSingleResult();
assertThat( sum ).isEqualTo( 3L );
// When not selected, group by should only use the foreign key (parent_id)
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "parent_id", 2 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", 0 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", 0 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 2 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount );
} );
}
@Test
public void testGroupByAndOrderBy(SessionFactoryScope scope) {
public void testGroupByAndOrderBySingleTable(SessionFactoryScope scope) {
testGroupByAndOrderBy( scope, "singleTableParent", SingleTableParent.class, "single_table_child_one", 1 );
}
@Test
public void testGroupByAndOrderByJoined(SessionFactoryScope scope) {
testGroupByAndOrderBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 );
}
@Test
public void testGroupByAndOrderByTPC(SessionFactoryScope scope) {
testGroupByAndOrderBy( scope, "tpcParent", TPCParent.class, "tpc_child_one", 4 );
}
private void testGroupByAndOrderBy(
SessionFactoryScope scope,
String parentProp,
Class<?> parentEntityClass,
String parentName,
int childPropCount) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( session -> {
final MyPojo myPojo = session.createQuery(
"select new "
+ MyPojo.class.getName()
+ "(sum(e.amount), re) from MyEntity e join e.parent re group by re order by re",
String.format(
"select new %s(sum(e.amount), re) from MyEntity e join e.%s re group by re order by re",
MyPojo.class.getName(),
parentProp
),
MyPojo.class
).getSingleResult();
assertThat( myPojo.getAmount() ).isEqualTo( 3L );
assertThat( myPojo.getParent().getName() ).isEqualTo( "child_one" );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", 3 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", 3 );
assertThat( myPojo.getParent().getName() ).isEqualTo( parentName );
final EntityMappingType entityMappingType = scope.getSessionFactory()
.getMappingMetamodel()
.findEntityDescriptor( parentEntityClass );
final int expectedCount = supportsFunctionalDependency( scope, entityMappingType ) ?
childPropCount :
childPropCount + 2;
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", expectedCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", expectedCount );
} );
}
@Test
public void testGroupByAndOrderByNotSelected(SessionFactoryScope scope) {
public void testGroupByAndOrderByNotSelectedSingleTable(SessionFactoryScope scope) {
testGroupByAndOrderByNotSelected( scope, "singleTableParent", "single_table_parent_id", 0 );
}
@Test
public void testGroupByAndOrderByNotSelectedJoined(SessionFactoryScope scope) {
testGroupByAndOrderByNotSelected( scope, "joinedParent", "joined_parent_id", 0 );
}
@Test
public void testGroupByAndOrderByNotSelectedTPC(SessionFactoryScope scope) {
testGroupByAndOrderByNotSelected( scope, "tpcParent", "tpc_parent_id", 3 );
}
private void testGroupByAndOrderByNotSelected(
SessionFactoryScope scope,
String parentProp,
String parentFkName,
int childPropCount) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( session -> {
final Long sum = session.createQuery(
"select sum(e.amount) from MyEntity e join e.parent re group by re order by re",
String.format(
"select sum(e.amount) from MyEntity e join e.%s re group by re order by re",
parentProp
),
Long.class
).getSingleResult();
assertThat( sum ).isEqualTo( 3L );
// When not selected, group by and order by should only use the foreign key (parent_id)
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "parent_id", 3 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", 0 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", 0 );
// When not selected, group by should only use the foreign key (parent_id)
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 3 );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount );
statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount );
} );
}
@Entity( name = "Parent" )
@DiscriminatorColumn( name = "disc_col", discriminatorType = DiscriminatorType.INTEGER )
private static Dialect getDialect(SessionFactoryScope scope) {
return scope.getSessionFactory().getJdbcServices().getDialect();
}
private static boolean supportsFunctionalDependency(
SessionFactoryScope scope,
EntityMappingType entityMappingType) {
final FunctionalDependencyAnalysisSupport analysisSupport = scope.getSessionFactory()
.getJdbcServices()
.getDialect()
.getFunctionalDependencyAnalysisSupport();
if ( analysisSupport.supportsAnalysis() ) {
if ( entityMappingType.getSqmMultiTableMutationStrategy() == null ) {
return true;
}
else {
return analysisSupport.supportsTableGroups() && ( analysisSupport.supportsConstants() ||
// Union entity persisters use a literal 'clazz_' column as a discriminator
// that breaks functional dependency for dialects that don't support constants
!( entityMappingType.getEntityPersister() instanceof UnionSubclassEntityPersister ) );
}
}
return false;
}
@MappedSuperclass
public abstract static class Parent {
@Id
@GeneratedValue
@ -136,13 +276,6 @@ public class InheritanceQueryGroupByTest {
private String name;
public Parent() {
}
public Parent(String name) {
this.name = name;
}
public Long getId() {
return id;
}
@ -150,36 +283,64 @@ public class InheritanceQueryGroupByTest {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity( name = "ChildOne" )
@Entity( name = "SingleTableParent" )
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name = "disc_col", discriminatorType = DiscriminatorType.INTEGER )
public static class SingleTableParent extends Parent {
}
@Entity( name = "SingleTableChildOne" )
@DiscriminatorValue( "1" )
public static class ChildOne extends Parent {
public static class SingleTableChildOne extends SingleTableParent {
@Column( name = "child_one_col" )
private Integer childOneProp;
public ChildOne() {
}
public ChildOne(String name, Integer childOneProp) {
super( name );
this.childOneProp = childOneProp;
}
}
@Entity( name = "ChildTwo" )
@Entity( name = "SingleTableChildTwo" )
@DiscriminatorValue( "2" )
public static class ChildTwo extends Parent {
public static class SingleTableChildTwo extends SingleTableParent {
@Column( name = "child_two_col" )
private Integer childTwoProp;
}
public ChildTwo() {
}
@Entity( name = "JoinedParent" )
@Inheritance( strategy = InheritanceType.JOINED )
public static class JoinedParent extends Parent {
}
public ChildTwo(String name, Integer childTwoProp) {
super( name );
this.childTwoProp = childTwoProp;
}
@Entity( name = "JoinedChildOne" )
public static class JoinedChildOne extends JoinedParent {
@Column( name = "child_one_col" )
private Integer childOneProp;
}
@Entity( name = "JoinedChildTwo" )
public static class JoinedChildTwo extends JoinedParent {
@Column( name = "child_two_col" )
private Integer childTwoProp;
}
@Entity( name = "TPCParent" )
@Inheritance( strategy = InheritanceType.TABLE_PER_CLASS )
public static class TPCParent extends Parent {
}
@Entity( name = "TPCChildOne" )
public static class TPCChildOne extends TPCParent {
@Column( name = "child_one_col" )
private Integer childOneProp;
}
@Entity( name = "TPCChildTwo" )
public static class TPCChildTwo extends TPCParent {
@Column( name = "child_two_col" )
private Integer childTwoProp;
}
@Entity( name = "MyEntity" )
@ -191,15 +352,29 @@ public class InheritanceQueryGroupByTest {
private Integer amount;
@ManyToOne
@JoinColumn( name = "parent_id" )
private Parent parent;
@JoinColumn( name = "single_table_parent_id" )
private SingleTableParent singleTableParent;
@ManyToOne
@JoinColumn( name = "joined_parent_id" )
private JoinedParent joinedParent;
@ManyToOne
@JoinColumn( name = "tpc_parent_id" )
private TPCParent tpcParent;
public MyEntity() {
}
public MyEntity(Integer amount, Parent parent) {
public MyEntity(
Integer amount,
SingleTableParent singleTableParent,
JoinedParent joinedParent,
TPCParent tpcParent) {
this.amount = amount;
this.parent = parent;
this.singleTableParent = singleTableParent;
this.joinedParent = joinedParent;
this.tpcParent = tpcParent;
}
}