HHH-15349 Fix rendering of EntityValuedPathInterpretation when comparing different model parts

This commit is contained in:
Christian Beikov 2022-06-22 12:04:27 +02:00
parent b3d0addaeb
commit 9cff075a89
17 changed files with 994 additions and 276 deletions

View File

@ -2,6 +2,8 @@ package org.hibernate.userguide.associations;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
@ -14,6 +16,7 @@ import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -29,6 +32,13 @@ import static org.junit.Assert.assertNull;
*/
public class NotFoundTest extends BaseEntityManagerFunctionalTestCase {
private SQLStatementInterceptor sqlStatementInterceptor;
@Override
protected void addConfigOptions(Map options) {
sqlStatementInterceptor = new SQLStatementInterceptor( options );
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
@ -111,19 +121,25 @@ public class NotFoundTest extends BaseEntityManagerFunctionalTestCase {
breakForeignKey();
inTransaction( entityManagerFactory(), (entityManager) -> {
sqlStatementInterceptor.clear();
//tag::associations-not-found-fk-function-example[]
final List<Person> nullResults = entityManager
.createSelectionQuery( "from Person p where fk( p.city ) is null", Person.class )
final List<String> nullResults = entityManager
.createSelectionQuery( "select p.name from Person p where fk( p.city ) is null", String.class )
.list();
assertThat( nullResults ).isEmpty();
final List<Person> nonNullResults = entityManager
.createSelectionQuery( "from Person p where fk( p.city ) is not null", Person.class )
final List<String> nonNullResults = entityManager
.createSelectionQuery( "select p.name from Person p where fk( p.city ) is not null", String.class )
.list();
assertThat( nonNullResults ).hasSize( 1 );
assertThat( nonNullResults.get( 0 ).getName() ).isEqualTo( "John Doe" );
assertThat( nonNullResults.get( 0 ) ).isEqualTo( "John Doe" );
//end::associations-not-found-fk-function-example[]
// In addition, make sure that the two executed queries do not create a join
assertThat( sqlStatementInterceptor.getQueryCount() ).isEqualTo( 2 );
assertThat( sqlStatementInterceptor.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
assertThat( sqlStatementInterceptor.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
} );
}

View File

@ -135,6 +135,10 @@ public class BinderHelper {
String syntheticPropertyName =
"_" + associatedClass.getEntityName().replace('.', '_') +
"_" + columns[0].getPropertyName().replace('.', '_');
if ( inverse ) {
// Use a different name for inverse synthetic properties to avoid duplicate properties for self-referencing models
syntheticPropertyName += "_inverse";
}
//find properties associated to a certain column
Object columnOwner = findColumnOwner( ownerEntity, columns[0].getReferencedColumn(), context );
List<Property> properties = findPropertiesByColumns( columnOwner, columns, context );

View File

@ -2271,7 +2271,7 @@ public abstract class CollectionBinder {
}
String referencedPropertyName =
buildingContext.getMetadataCollector().getPropertyReferencedAssociation(
"inverse__" + referencedEntity.getEntityName(), mappedBy
referencedEntity.getEntityName(), mappedBy
);
if ( referencedPropertyName != null ) {
//TODO always a many to one?

View File

@ -27,6 +27,10 @@ public interface EntityAssociationMapping extends ModelPart, Association, TableG
*/
ModelPart getKeyTargetMatchPart();
boolean isReferenceToPrimaryKey();
boolean isFkOptimizationAllowed();
@Override
default boolean incrementFetchDepth(){
return true;

View File

@ -50,6 +50,15 @@ public interface ForeignKeyDescriptor extends VirtualModelPart, ValueMapping {
String getTargetTable();
default String getTable(Nature nature) {
if ( nature == Nature.KEY ) {
return getKeyTable();
}
else {
return getTargetTable();
}
}
ModelPart getKeyPart();
ModelPart getTargetPart();

View File

@ -341,9 +341,14 @@ public class EmbeddedForeignKeyDescriptor implements ForeignKeyDescriptor {
SqlAstCreationContext creationContext) {
final TableReference lhsTableReference = targetSideTableGroup.resolveTableReference(
targetSideTableGroup.getNavigablePath(),
targetTable
targetTable,
false
);
final TableReference rhsTableKeyReference = keySideTableGroup.resolveTableReference(
null,
keyTable,
false
);
final TableReference rhsTableKeyReference = keySideTableGroup.resolveTableReference( keyTable );
return generateJoinPredicate(
lhsTableReference,
@ -459,27 +464,6 @@ public class EmbeddedForeignKeyDescriptor implements ForeignKeyDescriptor {
return true;
}
protected TableReference getTableReference(TableGroup lhs, TableGroup tableGroup, String table) {
TableReference tableReference = lhs.getPrimaryTableReference().resolveTableReference( table );
if ( tableReference != null ) {
return tableReference;
}
tableReference = tableGroup.getPrimaryTableReference().resolveTableReference( table );
if ( tableReference != null ) {
return tableReference;
}
tableReference = lhs.resolveTableReference(
lhs.getNavigablePath().append( getNavigableRole().getNavigableName() ),
table
);
if ( tableReference != null ) {
return tableReference;
}
throw new IllegalStateException( "Could not resolve binding for table `" + table + "`" );
}
@Override
public int visitKeySelectables(int offset, SelectableConsumer consumer) {
return keySelectableMappings.forEachSelectable( offset, consumer );

View File

@ -600,6 +600,20 @@ public class EntityCollectionPart
: ForeignKeyDescriptor.Nature.KEY;
}
@Override
public boolean isReferenceToPrimaryKey() {
return fkDescriptor.getSide( getSideNature().inverse() ).getModelPart() instanceof EntityIdentifierMapping;
}
@Override
public boolean isFkOptimizationAllowed() {
return !collectionDescriptor.isOneToMany();
}
public CollectionPersister getCollectionDescriptor() {
return collectionDescriptor;
}
@Override
public FetchStyle getStyle() {
return FetchStyle.JOIN;
@ -710,7 +724,9 @@ public class EntityCollectionPart
return false;
}
return targetKeyPropertyNames.contains( relativePath );
// Empty relative path means the navigable paths are equal,
// in which case we allow resolving the parent table group
return relativePath.isEmpty() || targetKeyPropertyNames.contains( relativePath );
},
this,
explicitSourceAlias,

View File

@ -316,10 +316,13 @@ public class SimpleForeignKeyDescriptor implements ForeignKeyDescriptor, BasicVa
SqlAstCreationContext creationContext) {
final TableReference lhsTableReference = targetSideTableGroup.resolveTableReference(
targetSideTableGroup.getNavigablePath(),
targetSide.getModelPart().getContainingTableExpression()
targetSide.getModelPart().getContainingTableExpression(),
false
);
final TableReference rhsTableKeyReference = keySideTableGroup.resolveTableReference(
keySide.getModelPart().getContainingTableExpression()
null,
keySide.getModelPart().getContainingTableExpression(),
false
);
return generateJoinPredicate(
@ -352,23 +355,6 @@ public class SimpleForeignKeyDescriptor implements ForeignKeyDescriptor, BasicVa
|| ( lhs.equals( targetExpression ) && rhs.equals( keyExpression ) );
}
protected TableReference getTableReference(TableGroup lhs, TableGroup tableGroup, String table) {
final NavigablePath navigablePath = lhs.getNavigablePath().append( getTargetPart().getFetchableName() );
if ( lhs.getPrimaryTableReference().getTableReference( navigablePath, table ) != null ) {
return lhs.getPrimaryTableReference();
}
else if ( tableGroup.getPrimaryTableReference().getTableReference( navigablePath, table ) != null ) {
return tableGroup.getPrimaryTableReference();
}
final TableReference tableReference = lhs.resolveTableReference( navigablePath, table );
if ( tableReference != null ) {
return tableReference;
}
throw new IllegalStateException( "Could not resolve binding for table `" + table + "`" );
}
@Override
public MappingType getPartMappingType() {
return targetSide.getModelPart().getMappedType();

View File

@ -703,10 +703,16 @@ public class ToOneAttributeMapping
return sideNature;
}
@Override
public boolean isReferenceToPrimaryKey() {
return foreignKeyDescriptor.getSide( sideNature.inverse() ).getModelPart() instanceof EntityIdentifierMapping;
}
@Override
public boolean isFkOptimizationAllowed() {
return canUseParentTableGroup;
}
public String getReferencedPropertyName() {
return referencedPropertyName;
}
@ -1576,7 +1582,9 @@ public class ToOneAttributeMapping
return false;
}
return targetKeyPropertyNames.contains( relativePath );
// Empty relative path means the navigable paths are equal,
// in which case we allow resolving the parent table group
return relativePath.isEmpty() || targetKeyPropertyNames.contains( relativePath );
}
),
null
@ -1606,23 +1614,31 @@ public class ToOneAttributeMapping
final TableReference lhsTableReference = lhs.resolveTableReference( navigablePath, identifyingColumnsTableExpression );
lazyTableGroup.setTableGroupInitializerCallback( (tableGroup) -> join.applyPredicate(
foreignKeyDescriptor.generateJoinPredicate(
sideNature == ForeignKeyDescriptor.Nature.TARGET ? lhsTableReference : tableGroup.getPrimaryTableReference(),
sideNature == ForeignKeyDescriptor.Nature.TARGET ? tableGroup.getPrimaryTableReference() : lhsTableReference,
sqlExpressionResolver,
creationContext
)
) );
lazyTableGroup.setTableGroupInitializerCallback(
tableGroup -> {
join.applyPredicate(
foreignKeyDescriptor.generateJoinPredicate(
sideNature == ForeignKeyDescriptor.Nature.TARGET ?
lhsTableReference :
tableGroup.getPrimaryTableReference(),
sideNature == ForeignKeyDescriptor.Nature.TARGET ?
tableGroup.getPrimaryTableReference() :
lhsTableReference,
sqlExpressionResolver,
creationContext
)
);
if ( hasNotFoundAction() ) {
getAssociatedEntityMappingType().applyWhereRestrictions(
join::applyPredicate,
lazyTableGroup.getTableGroup(),
true,
null
);
}
if ( hasNotFoundAction() ) {
getAssociatedEntityMappingType().applyWhereRestrictions(
join::applyPredicate,
tableGroup,
true,
null
);
}
}
);
return join;
}
@ -1697,7 +1713,9 @@ public class ToOneAttributeMapping
return false;
}
return targetKeyPropertyNames.contains( relativePath );
// Empty relative path means the navigable paths are equal,
// in which case we allow resolving the parent table group
return relativePath.isEmpty() || targetKeyPropertyNames.contains( relativePath );
},
tableGroupProducer,
explicitSourceAlias,

View File

@ -145,16 +145,16 @@ public class SqmMappingModelHelper {
MappingMetamodel domainModel,
Function<NavigablePath,TableGroup> tableGroupLocator) {
if ( sqmPath instanceof SqmTreatedPath ) {
final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmPath;
final EntityDomainType treatTargetType = treatedPath.getTreatTarget();
if ( sqmPath instanceof SqmTreatedPath<?, ?> ) {
final SqmTreatedPath<?, ?> treatedPath = (SqmTreatedPath<?, ?>) sqmPath;
final EntityDomainType<?> treatTargetType = treatedPath.getTreatTarget();
return domainModel.findEntityDescriptor( treatTargetType.getHibernateEntityName() );
}
// see if the LHS is treated
if ( sqmPath.getLhs() instanceof SqmTreatedPath ) {
final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmPath.getLhs();
final EntityDomainType treatTargetType = treatedPath.getTreatTarget();
if ( sqmPath.getLhs() instanceof SqmTreatedPath<?, ?> ) {
final SqmTreatedPath<?, ?> treatedPath = (SqmTreatedPath<?, ?>) sqmPath.getLhs();
final EntityDomainType<?> treatTargetType = treatedPath.getTreatTarget();
final EntityPersister container = domainModel.findEntityDescriptor( treatTargetType.getHibernateEntityName() );
return container.findSubPart( sqmPath.getNavigablePath().getLocalName(), container );

View File

@ -28,6 +28,7 @@ import org.hibernate.metamodel.mapping.ConvertibleModelPart;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
@ -350,6 +351,10 @@ public class SqmUtil {
if ( parameterType == null ) {
throw new SqlTreeCreationException( "Unable to interpret mapping-model type for Query parameter : " + domainParam );
}
else if ( parameterType instanceof PluralAttributeMapping ) {
// Default to the collection element
parameterType = ( (PluralAttributeMapping) parameterType ).getElementDescriptor();
}
if ( parameterType instanceof EntityIdentifierMapping ) {
final EntityIdentifierMapping identifierMapping = (EntityIdentifierMapping) parameterType;
@ -368,17 +373,20 @@ public class SqmUtil {
}
else if ( parameterType instanceof EntityAssociationMapping ) {
EntityAssociationMapping association = (EntityAssociationMapping) parameterType;
bindValue = association.getForeignKeyDescriptor().getAssociationKeyFromSide(
bindValue,
association.getSideNature().inverse(),
session
);
parameterType = association.getForeignKeyDescriptor();
}
else if ( parameterType instanceof PluralAttributeMapping ) {
// we'd expect the values to refer to the collection element
// for now, let's blow up and see where this happens and fix the specifics...
throw new NotYetImplementedFor6Exception( "Binding parameters whose inferred type comes from plural attribute not yet implemented" );
if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) {
// If the association is the target, we must use the identifier of the EntityMappingType
bindValue = association.getAssociatedEntityMappingType().getIdentifierMapping()
.getIdentifier( bindValue );
parameterType = association.getAssociatedEntityMappingType().getIdentifierMapping();
}
else {
bindValue = association.getForeignKeyDescriptor().getAssociationKeyFromSide(
bindValue,
association.getSideNature().inverse(),
session
);
parameterType = association.getForeignKeyDescriptor();
}
}
int offset = jdbcParameterBindings.registerParametersForEachJdbcValue(

View File

@ -332,6 +332,7 @@ import org.hibernate.sql.ast.tree.from.SyntheticVirtualTableGroup;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.from.VirtualTableGroup;
import org.hibernate.sql.ast.tree.insert.InsertStatement;
@ -1080,8 +1081,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
rootTableGroup
);
if ( !rootTableGroup.getTableReferenceJoins().isEmpty()
|| !rootTableGroup.getTableGroupJoins().isEmpty() ) {
if ( hasJoins( rootTableGroup ) ) {
throw new SemanticException( "Not expecting multiple table references for an SQM INSERT-SELECT" );
}
}
@ -1109,6 +1109,24 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
return insertStatement;
}
private static boolean hasJoins(TableGroup rootTableGroup) {
if ( !rootTableGroup.getTableReferenceJoins().isEmpty() ) {
return true;
}
return hasJoins( rootTableGroup.getTableGroupJoins() ) || hasJoins( rootTableGroup.getNestedTableGroupJoins() );
}
private static boolean hasJoins(List<TableGroupJoin> tableGroupJoins) {
for ( TableGroupJoin tableGroupJoin : tableGroupJoins ) {
final TableGroup joinedGroup = tableGroupJoin.getJoinedGroup();
if ( joinedGroup instanceof LazyTableGroup && ( (LazyTableGroup) joinedGroup ).getUnderlyingTableGroup() == null ) {
continue;
}
return true;
}
return false;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Insert-values statement
@ -3097,6 +3115,32 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
implicitJoinChecker = BaseSqmToSqlAstConverter::verifyManipulationImplicitJoin;
}
prepareReusablePath( fromClauseIndex, sqmPath, implicitJoinChecker );
// Create the table group for every path that can potentially require one,
// as some paths require joining the target table i.e. inverse one-to-one
// Note that this will not necessarily create joins immediately, as table groups are lazy
if ( sqmPath instanceof SqmEntityValuedSimplePath<?>
|| sqmPath instanceof SqmEmbeddedValuedSimplePath<?>
|| sqmPath instanceof SqmAnyValuedSimplePath<?> ) {
final TableGroup existingTableGroup = fromClauseIndex.findTableGroup( sqmPath.getNavigablePath() );
if ( existingTableGroup == null ) {
final TableGroup createdTableGroup = createTableGroup(
fromClauseIndex.getTableGroup( sqmPath.getLhs().getNavigablePath() ),
sqmPath
);
if ( createdTableGroup != null ) {
if ( sqmPath instanceof SqmTreatedPath<?, ?> ) {
fromClauseIndex.register( sqmPath, createdTableGroup );
}
if ( sqmPath instanceof SqmFrom<?, ?> ) {
registerTreatUsage( (SqmFrom<?, ?>) sqmPath, createdTableGroup );
}
}
}
else if ( sqmPath instanceof SqmFrom<?, ?> ) {
registerTreatUsage( (SqmFrom<?, ?>) sqmPath, existingTableGroup );
}
}
return supplier.get();
}
@ -3190,26 +3234,29 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
if ( tableGroup == null ) {
prepareReusablePath( path, () -> null );
final NavigablePath navigablePath;
if ( CollectionPart.Nature.fromNameExact( path.getNavigablePath().getLocalName() ) != null ) {
navigablePath = path.getLhs().getLhs().getNavigablePath();
}
else if ( path instanceof SqmTreatedRoot<?, ?> ) {
navigablePath = ( (SqmTreatedRoot<?, ?>) path ).getWrappedPath().getNavigablePath();
}
else {
navigablePath = path.getLhs().getNavigablePath();
}
final TableGroup createdTableGroup = createTableGroup(
fromClauseIndex.getTableGroup( navigablePath ),
path
);
if ( createdTableGroup != null ) {
if ( path instanceof SqmTreatedPath<?, ?> ) {
fromClauseIndex.register( path, createdTableGroup );
if ( !( path instanceof SqmEntityValuedSimplePath<?>
|| path instanceof SqmEmbeddedValuedSimplePath<?>
|| path instanceof SqmAnyValuedSimplePath<?> ) ) {
// Since this is a selection, we must create a table group for the path as a DomainResult will be created
// But only create it for paths that are not handled by #prepareReusablePath anyway
final NavigablePath navigablePath;
if ( path instanceof SqmTreatedRoot<?, ?> ) {
navigablePath = ( (SqmTreatedRoot<?, ?>) path ).getWrappedPath().getNavigablePath();
}
if ( path instanceof SqmFrom<?, ?> ) {
registerTreatUsage( (SqmFrom<?, ?>) path, createdTableGroup );
else {
navigablePath = path.getLhs().getNavigablePath();
}
final TableGroup createdTableGroup = createTableGroup(
fromClauseIndex.getTableGroup( navigablePath ),
path
);
if ( createdTableGroup != null ) {
if ( path instanceof SqmTreatedPath<?, ?> ) {
fromClauseIndex.register( path, createdTableGroup );
}
if ( path instanceof SqmFrom<?, ?> ) {
registerTreatUsage( (SqmFrom<?, ?>) path, createdTableGroup );
}
}
}
}
@ -3288,7 +3335,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
fromClauseIndex.register( joinedPath, tableGroup );
registerPluralTableGroupParts( tableGroup );
registerPluralTableGroupParts( joinedPath.getNavigablePath(), tableGroup );
}
else {
tableGroup = null;
@ -3322,17 +3369,25 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
private void registerPluralTableGroupParts(TableGroup tableGroup) {
registerPluralTableGroupParts( null, tableGroup );
}
private void registerPluralTableGroupParts(NavigablePath navigablePath, TableGroup tableGroup) {
if ( tableGroup instanceof PluralTableGroup ) {
final PluralTableGroup pluralTableGroup = (PluralTableGroup) tableGroup;
if ( pluralTableGroup.getElementTableGroup() != null ) {
getFromClauseAccess().registerTableGroup(
pluralTableGroup.getElementTableGroup().getNavigablePath(),
navigablePath == null || navigablePath == tableGroup.getNavigablePath()
? pluralTableGroup.getElementTableGroup().getNavigablePath()
: navigablePath.append( CollectionPart.Nature.ELEMENT.getName() ),
pluralTableGroup.getElementTableGroup()
);
}
if ( pluralTableGroup.getIndexTableGroup() != null ) {
getFromClauseAccess().registerTableGroup(
pluralTableGroup.getIndexTableGroup().getNavigablePath(),
navigablePath == null || navigablePath == tableGroup.getNavigablePath()
? pluralTableGroup.getIndexTableGroup().getNavigablePath()
: navigablePath.append( CollectionPart.Nature.INDEX.getName() ),
pluralTableGroup.getIndexTableGroup()
);
}
@ -3422,95 +3477,130 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
private Expression visitTableGroup(TableGroup tableGroup, SqmFrom<?, ?> path) {
final ModelPartContainer modelPart;
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
// For plain SqmFrom node uses, prefer the mapping type from the context if possible
if ( !( inferredValueMapping instanceof ModelPartContainer ) ) {
modelPart = tableGroup.getModelPart();
}
else {
modelPart = (ModelPartContainer) inferredValueMapping;
}
final ModelPart resultModelPart;
final ModelPart interpretationModelPart;
final TableGroup parentGroupToUse;
if ( modelPart instanceof ToOneAttributeMapping ) {
final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) modelPart;
final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart(
toOneAttributeMapping.getSideNature().inverse()
);
if ( tableGroup.getModelPart().getPartMappingType() == modelPart.getPartMappingType() ) {
resultModelPart = targetPart;
}
else {
// If the table group is for a different mapping type i.e. an inheritance subtype,
// lookup the target part on that mapping type
resultModelPart = tableGroup.getModelPart().findSubPart( targetPart.getPartName(), null );
}
interpretationModelPart = modelPart;
parentGroupToUse = null;
}
else if ( modelPart instanceof PluralAttributeMapping ) {
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) modelPart;
final CollectionPart elementDescriptor = pluralAttributeMapping.getElementDescriptor();
if ( elementDescriptor instanceof EntityCollectionPart ) {
// Usually, we need to resolve to the PK for visitTableGroup
final EntityCollectionPart collectionPart = (EntityCollectionPart) elementDescriptor;
final ModelPart collectionTargetPart = collectionPart.getForeignKeyDescriptor()
.getPart( collectionPart.getSideNature().inverse() );
final EntityIdentifierMapping identifierMapping = collectionPart.getEntityMappingType()
.getIdentifierMapping();
// If the FK points to the PK, we can use the FK part though, if this is not a root
if ( collectionTargetPart == identifierMapping && !( path instanceof SqmRoot<?> ) ) {
resultModelPart = collectionPart.getForeignKeyDescriptor().getPart( collectionPart.getSideNature() );
}
else {
resultModelPart = identifierMapping;
}
}
else {
resultModelPart = elementDescriptor;
}
interpretationModelPart = elementDescriptor;
parentGroupToUse = null;
}
else if ( modelPart instanceof EntityCollectionPart ) {
// Usually, we need to resolve to the PK for visitTableGroup
final EntityCollectionPart collectionPart = (EntityCollectionPart) modelPart;
final ModelPart collectionTargetPart = collectionPart.getForeignKeyDescriptor()
.getPart( collectionPart.getSideNature().inverse() );
final EntityIdentifierMapping identifierMapping = collectionPart.getEntityMappingType()
.getIdentifierMapping();
// If the FK points to the PK, we can use the FK part though, if this is not a root
if ( collectionTargetPart == identifierMapping && !( path instanceof SqmRoot<?> ) ) {
resultModelPart = collectionPart.getForeignKeyDescriptor().getPart( collectionPart.getSideNature() );
}
else {
resultModelPart = identifierMapping;
}
interpretationModelPart = modelPart;
parentGroupToUse = findTableGroup( tableGroup.getNavigablePath().getParent() );
}
else if ( modelPart instanceof EntityMappingType ) {
resultModelPart = ( (EntityMappingType) modelPart ).getIdentifierMapping();
interpretationModelPart = modelPart;
parentGroupToUse = null;
}
else {
resultModelPart = modelPart;
interpretationModelPart = modelPart;
parentGroupToUse = null;
}
final ModelPartContainer tableGroupModelPart = tableGroup.getModelPart();
final ModelPart actualModelPart;
final NavigablePath navigablePath;
if ( interpretationModelPart == modelPart ) {
if ( tableGroupModelPart instanceof PluralAttributeMapping ) {
actualModelPart = ( (PluralAttributeMapping) tableGroupModelPart ).getElementDescriptor();
navigablePath = tableGroup.getNavigablePath().append( actualModelPart.getPartName() );
}
else {
actualModelPart = tableGroupModelPart;
navigablePath = tableGroup.getNavigablePath();
}
else {
navigablePath = tableGroup.getNavigablePath().append( interpretationModelPart.getPartName() );
}
final Expression result;
if ( interpretationModelPart instanceof EntityValuedModelPart ) {
if ( actualModelPart instanceof EntityValuedModelPart ) {
final EntityValuedModelPart entityValuedModelPart = (EntityValuedModelPart) actualModelPart;
final EntityValuedModelPart inferredEntityMapping = (EntityValuedModelPart) getInferredValueMapping();
final ModelPart resultModelPart;
final ModelPart interpretationModelPart;
final TableGroup tableGroupToUse;
if ( inferredEntityMapping == null ) {
// When the inferred mapping is null, we try to resolve to the FK by default,
// which is fine because expansion to all target columns for select and group by clauses is handled below
if ( entityValuedModelPart instanceof EntityAssociationMapping && ( (EntityAssociationMapping) entityValuedModelPart ).getSideNature() != ForeignKeyDescriptor.Nature.TARGET ) {
// If the table group uses an association mapping that is not a one-to-many,
// we make use of the FK model part
final EntityAssociationMapping associationMapping = (EntityAssociationMapping) entityValuedModelPart;
final ModelPart targetPart = associationMapping.getForeignKeyDescriptor().getPart(
associationMapping.getSideNature()
);
if ( entityValuedModelPart.getPartMappingType() == associationMapping.getPartMappingType() ) {
resultModelPart = targetPart;
}
else {
// If the table group is for a different mapping type i.e. an inheritance subtype,
// lookup the target part on that mapping type
resultModelPart = entityValuedModelPart.findSubPart( targetPart.getPartName(), null );
}
}
else {
// Otherwise, we use the identifier mapping of the target entity type
resultModelPart = entityValuedModelPart.getEntityMappingType().getIdentifierMapping();
}
interpretationModelPart = entityValuedModelPart;
tableGroupToUse = null;
}
else if ( inferredEntityMapping instanceof ToOneAttributeMapping ) {
// If the inferred mapping is a to-one association,
// we use the FK target part, which must be located on the entity mapping
final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) inferredEntityMapping;
final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart(
toOneAttributeMapping.getSideNature().inverse()
);
if ( entityValuedModelPart.getPartMappingType() == toOneAttributeMapping.getPartMappingType() ) {
resultModelPart = targetPart;
}
else {
// If the table group is for a different mapping type i.e. an inheritance subtype,
// lookup the target part on that mapping type
resultModelPart = entityValuedModelPart.findSubPart( targetPart.getPartName(), null );
}
interpretationModelPart = toOneAttributeMapping;
tableGroupToUse = null;
}
else if ( inferredEntityMapping instanceof EntityCollectionPart ) {
// If the inferred mapping is a collection part, we try to make use of the FK again to avoid joins
final EntityCollectionPart collectionPart = (EntityCollectionPart) inferredEntityMapping;
final TableGroup collectionTableGroup;
if ( tableGroup.getModelPart() instanceof CollectionPart ) {
collectionTableGroup = findTableGroup( tableGroup.getNavigablePath().getParent() );
}
else {
collectionTableGroup = tableGroup;
}
if ( entityValuedModelPart == collectionPart ) {
// When we compare the same collection parts, we can just use the FK part
resultModelPart = collectionPart.getForeignKeyDescriptor()
.getPart( collectionPart.getSideNature() );
}
else if ( entityValuedModelPart instanceof EntityAssociationMapping ) {
// If the table group model part is an association, we check if the FK targets are compatible
final EntityAssociationMapping tableGroupAssociation = (EntityAssociationMapping) entityValuedModelPart;
final ModelPart pathTargetPart = tableGroupAssociation.getForeignKeyDescriptor()
.getPart( tableGroupAssociation.getSideNature().inverse() );
final ModelPart inferredTargetPart = collectionPart.getForeignKeyDescriptor()
.getPart( collectionPart.getSideNature().inverse() );
// If the inferred association and table group association targets are the same,
// or the table group association refers to the primary key, we can safely use the FK part
if ( pathTargetPart == inferredTargetPart || tableGroupAssociation.isReferenceToPrimaryKey() ) {
resultModelPart = tableGroupAssociation.getForeignKeyDescriptor()
.getPart( tableGroupAssociation.getSideNature() );
}
else {
// Otherwise, we must force the use of the identifier mapping and possibly create a join,
// because comparing by primary key is the only sensible thing to do in this case.
// Note that EntityValuedPathInterpretation does the same
resultModelPart = collectionPart.getAssociatedEntityMappingType().getIdentifierMapping();
}
}
else {
// Since the table group model part is an EntityMappingType,
// we can render the FK target model part of the inferred collection part,
// which might be a UK, but usually a PK
assert entityValuedModelPart instanceof EntityMappingType;
if ( collectionPart.getCollectionDescriptor().isOneToMany() ) {
// When the inferred mapping is a one-to-many collection part,
// we will render the entity identifier mapping for that collection part,
// so we will have to do the same for the EntityMappingType side
resultModelPart = collectionPart.getAssociatedEntityMappingType().getIdentifierMapping();
}
else {
resultModelPart = collectionPart.getForeignKeyDescriptor()
.getPart( collectionPart.getSideNature().inverse() );
}
}
interpretationModelPart = inferredEntityMapping;
tableGroupToUse = collectionTableGroup;
}
else {
// Render the identifier mapping if the inferred mapping is an EntityMappingType
assert inferredEntityMapping instanceof EntityMappingType;
resultModelPart = ( (EntityMappingType) inferredEntityMapping ).getIdentifierMapping();
interpretationModelPart = inferredEntityMapping;
tableGroupToUse = null;
}
final boolean expandToAllColumns;
if ( currentClauseStack.getCurrent() == Clause.GROUP ) {
// When the table group is known to be fetched i.e. a fetch join
@ -3523,28 +3613,29 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
final EntityValuedModelPart mapping = (EntityValuedModelPart) interpretationModelPart;
EntityMappingType mappingType;
final EntityMappingType treatedMapping;
if ( path instanceof SqmTreatedPath ) {
mappingType = creationContext.getSessionFactory()
treatedMapping = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel()
.findEntityDescriptor( ( (SqmTreatedPath<?,?>) path ).getTreatTarget().getHibernateEntityName() );
}
else {
mappingType = mapping.getEntityMappingType();
treatedMapping = mapping.getEntityMappingType();
}
result = EntityValuedPathInterpretation.from(
navigablePath,
parentGroupToUse == null ? tableGroup : parentGroupToUse,
tableGroupToUse == null ? tableGroup : tableGroupToUse,
expandToAllColumns ? null : resultModelPart,
true,
(EntityValuedModelPart) interpretationModelPart,
mappingType,
treatedMapping,
this
);
}
else if ( interpretationModelPart instanceof EmbeddableValuedModelPart ) {
final EmbeddableValuedModelPart mapping = (EmbeddableValuedModelPart) resultModelPart;
else if ( actualModelPart instanceof EmbeddableValuedModelPart ) {
final EmbeddableValuedModelPart mapping = (EmbeddableValuedModelPart) actualModelPart;
result = new EmbeddableValuedPathInterpretation<>(
mapping.toSqlExpression(
tableGroup,
@ -3553,15 +3644,15 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
getSqlAstCreationState()
),
navigablePath,
(EmbeddableValuedModelPart) interpretationModelPart,
mapping,
tableGroup
);
}
else {
assert interpretationModelPart instanceof BasicValuedModelPart;
final BasicValuedModelPart mapping = (BasicValuedModelPart) resultModelPart;
assert actualModelPart instanceof BasicValuedModelPart;
final BasicValuedModelPart mapping = (BasicValuedModelPart) actualModelPart;
final TableReference tableReference = tableGroup.resolveTableReference(
navigablePath.append( resultModelPart.getPartName() ),
navigablePath.append( actualModelPart.getPartName() ),
mapping.getContainingTableExpression()
);
@ -3591,7 +3682,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
result = new BasicValuedPathInterpretation<>(
columnReference,
navigablePath,
(BasicValuedModelPart) interpretationModelPart,
mapping,
tableGroup
);
}
@ -3599,7 +3690,6 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
return withTreatRestriction( result, path );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SqmPath
@ -3756,15 +3846,18 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
@Override
public Object visitFkExpression(SqmFkExpression<?> fkExpression) {
final EntityValuedPathInterpretation<?> toOneInterpretation = (EntityValuedPathInterpretation<?>) visitEntityValuedPath( fkExpression.getToOnePath() );
assert toOneInterpretation.getExpressionType() instanceof ToOneAttributeMapping;
final SqmPath<?> lhs = fkExpression.getToOnePath().getLhs();
prepareReusablePath( lhs, () -> null );
final TableGroup tableGroup = getFromClauseIndex().findTableGroup( lhs.getNavigablePath() );
final ModelPart subPart = tableGroup.getModelPart()
.findSubPart( fkExpression.getToOnePath().getModel().getPathName(), null );
assert subPart instanceof ToOneAttributeMapping;
final ToOneAttributeMapping toOneMapping = (ToOneAttributeMapping) toOneInterpretation.getExpressionType();
final ToOneAttributeMapping toOneMapping = (ToOneAttributeMapping) subPart;
final ForeignKeyDescriptor fkDescriptor = toOneMapping.getForeignKeyDescriptor();
final TableGroup tableGroup = toOneInterpretation.getTableGroup();
final TableReference tableReference = tableGroup.resolveTableReference( fkDescriptor.getKeyTable() );
final TableReference tableReference = tableGroup.resolveTableReference( fkDescriptor.getTable( toOneMapping.getSideNature() ) );
final ModelPart fkKeyPart = fkDescriptor.getKeyPart();
final ModelPart fkKeyPart = fkDescriptor.getPart( toOneMapping.getSideNature() );
if ( fkKeyPart instanceof BasicValuedModelPart ) {
final BasicValuedModelPart basicFkPart = (BasicValuedModelPart) fkKeyPart;
@ -4686,15 +4779,23 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final EntityValuedModelPart entityValuedModelPart = (EntityValuedModelPart) expressible;
final Object associationKey;
final ModelPart associationKeyPart;
if ( entityValuedModelPart instanceof Association ) {
final Association association = (Association) entityValuedModelPart;
if ( entityValuedModelPart instanceof EntityAssociationMapping ) {
final EntityAssociationMapping association = (EntityAssociationMapping) entityValuedModelPart;
final ForeignKeyDescriptor foreignKeyDescriptor = association.getForeignKeyDescriptor();
associationKey = foreignKeyDescriptor.getAssociationKeyFromSide(
literal.getLiteralValue(),
association.getSideNature().inverse(),
null
);
associationKeyPart = foreignKeyDescriptor.getPart( association.getSideNature() );
if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) {
// If the association is the target, we must use the identifier of the EntityMappingType
associationKey = association.getAssociatedEntityMappingType().getIdentifierMapping()
.getIdentifier( literal.getLiteralValue() );
associationKeyPart = association.getAssociatedEntityMappingType().getIdentifierMapping();
}
else {
associationKey = foreignKeyDescriptor.getAssociationKeyFromSide(
literal.getLiteralValue(),
association.getSideNature().inverse(),
null
);
associationKeyPart = foreignKeyDescriptor.getPart( association.getSideNature() );
}
}
else {
final EntityIdentifierMapping identifierMapping = entityValuedModelPart.getEntityMappingType()
@ -4974,11 +5075,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final MappingModelExpressible<?> inferredMapping = resolveInferredType();
if ( inferredMapping != null ) {
if ( inferredMapping instanceof PluralAttributeMapping ) {
final CollectionPart elementDescriptor = ( (PluralAttributeMapping) inferredMapping ).getElementDescriptor();
if ( elementDescriptor instanceof EntityCollectionPart ) {
return ( (EntityCollectionPart) elementDescriptor ).getEntityMappingType();
}
return elementDescriptor;
return ( (PluralAttributeMapping) inferredMapping ).getElementDescriptor();
}
else if ( !( inferredMapping instanceof JavaObjectType ) ) {
// Never report back the "object type" as inferred type and instead rely on the value type
@ -5005,7 +5102,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
if ( paramType == null ) {
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
if ( inferredValueMapping != null ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
// Default to the Object type
return basicType( Object.class );
@ -5019,7 +5116,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final JdbcMapping inferredJdbcMapping = inferredValueMapping.getJdbcMappings().get( 0 );
// If the bind type has a different JDBC type, we prefer that
if ( paramJdbcMapping.getJdbcType() == inferredJdbcMapping.getJdbcType() ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
}
return paramModelType;
@ -5030,7 +5127,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// inferrable type and fallback to resolving the binding type
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
if ( inferredValueMapping != null ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
}
else if ( paramType instanceof EntityDomainType ) {
@ -5039,7 +5136,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// otherwise this would default to binding the PK which might be wrong
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
if ( inferredValueMapping != null ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
}
@ -5060,7 +5157,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
modelPart = getFromClauseAccess().getTableGroup( navigablePath ).getModelPart();
}
if ( modelPart instanceof PluralAttributeMapping ) {
return ( (PluralAttributeMapping) modelPart ).getElementDescriptor();
return resolveInferredValueMappingForParameter( ( (PluralAttributeMapping) modelPart ).getElementDescriptor() );
}
return modelPart;
}
@ -5073,7 +5170,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// Try to infer the value mapping since the other side apparently is a path source
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
if ( inferredValueMapping != null ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
throw new NotYetImplementedFor6Exception( "Support for embedded-valued parameters not yet implemented" );
}
@ -5082,21 +5179,21 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// Try to infer the value mapping since the other side apparently is a path source
final MappingModelExpressible<?> inferredValueMapping = getInferredValueMapping();
if ( inferredValueMapping != null ) {
return inferredValueMapping;
return resolveInferredValueMappingForParameter( inferredValueMapping );
}
final BasicType basicTypeForJavaType = getTypeConfiguration().getBasicTypeForJavaType(
final BasicType<?> basicTypeForJavaType = getTypeConfiguration().getBasicTypeForJavaType(
paramSqmType.getExpressibleJavaType().getJavaTypeClass()
);
if ( basicTypeForJavaType == null ) {
if ( paramSqmType instanceof EntityDomainType ) {
return getIdType( (EntityDomainType) paramSqmType );
return resolveEntityPersister( (EntityDomainType<?>) paramSqmType );
}
else if ( paramSqmType instanceof SingularAttribute ) {
final Type type = ( (SingularAttribute) paramSqmType ).getType();
final Type<?> type = ( (SingularAttribute<?, ?>) paramSqmType ).getType();
if ( type instanceof EntityDomainType ) {
return getIdType( (EntityDomainType) type );
return resolveEntityPersister( (EntityDomainType<?>) type );
}
}
}
@ -5107,13 +5204,16 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
throw new ConversionException( "Could not determine ValueMapping for SqmParameter: " + sqmParameter );
}
private BasicType getIdType(EntityDomainType entityDomainType) {
final SimpleDomainType idType = entityDomainType.getIdType();
if ( idType != null ) {
return getTypeConfiguration().getBasicTypeForJavaType(
idType.getExpressibleJavaType().getJavaTypeClass() );
private MappingModelExpressible<?> resolveInferredValueMappingForParameter(MappingModelExpressible<?> inferredValueMapping) {
if ( inferredValueMapping instanceof PluralAttributeMapping ) {
// For parameters, we resolve to the element descriptor
inferredValueMapping = ( (PluralAttributeMapping) inferredValueMapping ).getElementDescriptor();
}
return null;
if ( inferredValueMapping instanceof EntityCollectionPart ) {
// For parameters, we resolve to the entity mapping type to bind the primary key
return ( (EntityCollectionPart) inferredValueMapping ).getAssociatedEntityMappingType();
}
return inferredValueMapping;
}
private void resolveSqmParameter(
@ -6293,16 +6393,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) determineValueMapping(
pluralPath );
if ( pluralAttributeMapping.getElementDescriptor() instanceof EntityCollectionPart ) {
inferrableTypeAccessStack.push(
() -> ( (EntityCollectionPart) pluralAttributeMapping.getElementDescriptor() ).getKeyTargetMatchPart() );
}
else if ( pluralAttributeMapping.getElementDescriptor() instanceof EmbeddedCollectionPart ) {
inferrableTypeAccessStack.push(pluralAttributeMapping::getElementDescriptor);
}
else {
inferrableTypeAccessStack.push( () -> pluralAttributeMapping );
}
inferrableTypeAccessStack.push( () -> pluralAttributeMapping );
final Expression lhs;
try {

View File

@ -13,6 +13,7 @@ import java.util.function.Consumer;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
@ -21,16 +22,21 @@ import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.SelectableConsumer;
import org.hibernate.metamodel.mapping.internal.EntityCollectionPart;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.spi.NavigablePath;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.spi.FromClauseAccess;
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.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.from.CorrelatedTableGroup;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.update.Assignable;
@ -50,13 +56,8 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
SqmToSqlAstConverter sqlAstCreationState) {
final TableGroup tableGroup = sqlAstCreationState
.getFromClauseAccess()
.findTableGroup( sqmPath.getLhs().getNavigablePath() );
final EntityValuedModelPart pathMapping = (EntityValuedModelPart) sqlAstCreationState
.getFromClauseAccess()
.findTableGroup( sqmPath.getLhs().getNavigablePath() )
.getModelPart()
.findSubPart( sqmPath.getReferencedPathSource().getPathName(), null );
final EntityValuedModelPart mapping;
.findTableGroup( sqmPath.getNavigablePath() );
final EntityValuedModelPart pathMapping = (EntityValuedModelPart) tableGroup.getModelPart();
if ( inferredMapping instanceof EntityAssociationMapping ) {
final EntityAssociationMapping inferredAssociation = (EntityAssociationMapping) inferredMapping;
if ( pathMapping instanceof EntityAssociationMapping && inferredMapping != pathMapping ) {
@ -67,54 +68,207 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
.getPart( pathAssociation.getSideNature().inverse() );
final ModelPart inferredTargetPart = inferredAssociation.getForeignKeyDescriptor()
.getPart( inferredAssociation.getSideNature().inverse() );
// If the inferred association and path association targets are the same, we can use the path mapping type
// which will render the FK of the path association
if ( pathTargetPart == inferredTargetPart ) {
mapping = pathMapping;
return from(
sqmPath.getNavigablePath(),
tableGroup,
pathMapping,
inferredMapping,
sqlAstCreationState
);
}
// When the inferred mapping and the path mapping differ,
// we always render the PK for the inferred and mapping path
else {
// Otherwise, we need to use the entity mapping type to force rendering the PK
// for e.g. `a.assoc1 = a.assoc2` when both associations have different target join columns
mapping = pathMapping.getEntityMappingType();
// If the path mapping refers to the primary key though,
// we can also render FK as that is equivalent
if ( pathAssociation.isReferenceToPrimaryKey() ) {
return from(
sqmPath.getNavigablePath(),
tableGroup,
pathMapping,
inferredMapping,
sqlAstCreationState
);
}
else {
// but if the association FK does not point to the primary key,
// we can't allow FK optimizations as that is problematic for self-referential associations,
// because then we would render the PK of the association owner instead of the target
return from(
sqmPath.getNavigablePath(),
tableGroup,
pathMapping.getEntityMappingType().getIdentifierMapping(),
false,
pathMapping,
pathMapping,
sqlAstCreationState
);
}
}
}
else {
// This is the case when the inferred mapping is an association, but the path mapping is not,
// or the path mapping and the inferred mapping are for the same association
mapping = (EntityValuedModelPart) inferredMapping;
// or the path mapping and the inferred mapping are for the same association,
// in which case we render this path like the inferred mapping
return from(
sqmPath.getNavigablePath(),
tableGroup,
(EntityValuedModelPart) inferredMapping,
inferredMapping,
sqlAstCreationState
);
}
}
else {
mapping = pathMapping;
// No inferred mapping available or it refers to an EntityMappingType
return from(
sqmPath.getNavigablePath(),
tableGroup,
pathMapping,
inferredMapping,
sqlAstCreationState
);
}
}
private static <T> EntityValuedPathInterpretation<T> from(
NavigablePath navigablePath,
TableGroup tableGroup,
EntityValuedModelPart mapping,
MappingModelExpressible<?> inferredMapping,
SqmToSqlAstConverter sqlAstCreationState) {
final boolean allowFkOptimization;
final ModelPart resultModelPart;
final TableGroup resultTableGroup;
// For association mappings where the FK optimization i.e. use of the parent table group is allowed,
// we try to make use of it and the FK model part if possible based on the inferred mapping
if ( mapping instanceof EntityAssociationMapping ) {
final EntityAssociationMapping associationMapping = (EntityAssociationMapping) mapping;
final ModelPart keyTargetMatchPart = associationMapping.getKeyTargetMatchPart();
if ( keyTargetMatchPart instanceof ToOneAttributeMapping ) {
resultModelPart = ( (ToOneAttributeMapping) keyTargetMatchPart ).getKeyTargetMatchPart();
final ModelPart associationKeyTargetMatchPart = associationMapping.getKeyTargetMatchPart();
final ModelPart keyTargetMatchPart;
if ( associationKeyTargetMatchPart instanceof ToOneAttributeMapping ) {
keyTargetMatchPart = ( (ToOneAttributeMapping) associationKeyTargetMatchPart ).getKeyTargetMatchPart();
}
else {
keyTargetMatchPart = associationKeyTargetMatchPart;
}
if ( associationMapping.isFkOptimizationAllowed() ) {
final boolean forceUsingForeignKeyAssociationSidePart;
// The following is an optimization for EntityCollectionPart path mappings with a join table.
// We can possibly avoid doing the join of the target table by using the parent table group
// and forcing to use the foreign key part of the association side
if ( inferredMapping != null && hasJoinTable( associationMapping ) ) {
// But we need to make sure the inferred mapping points to the same FK target
if ( inferredMapping instanceof EntityMappingType ) {
// If the inferred mapping is an EntityMappingType, meaning it's some sort of root path,
// we only have to make sure the FK target is part of the mapping type
forceUsingForeignKeyAssociationSidePart = ( (EntityMappingType) inferredMapping ).findSubPart( keyTargetMatchPart.getPartName() ) != null;
}
else {
// Otherwise, it must be an association mapping
assert inferredMapping instanceof EntityAssociationMapping;
// Comparing UK-based toOne with PK-based collection part
// In this case, we compare based on PK/FK, and can use the FK part if it points to the PK
forceUsingForeignKeyAssociationSidePart = ( (EntityAssociationMapping) inferredMapping ).getKeyTargetMatchPart() != keyTargetMatchPart
&& associationMapping.isReferenceToPrimaryKey();
}
}
else {
forceUsingForeignKeyAssociationSidePart = false;
}
if ( forceUsingForeignKeyAssociationSidePart ) {
if ( associationKeyTargetMatchPart instanceof ToOneAttributeMapping ) {
resultModelPart = keyTargetMatchPart;
}
else {
resultModelPart = associationMapping.getForeignKeyDescriptor()
.getPart( associationMapping.getSideNature() );
}
resultTableGroup = sqlAstCreationState.getFromClauseAccess()
.findTableGroup( tableGroup.getNavigablePath().getParent() );
}
else {
if ( isCorrelated( tableGroup, sqlAstCreationState ) ) {
// Access to the parent table group is forbidden for correlated table groups. For more details,
// see: `ToOneAttributeMapping.createRootTableGroupJoin`
// Due to that, we forcefully use the model part to which this association points to i.e. the target
resultModelPart = associationMapping.getForeignKeyDescriptor()
.getPart( associationMapping.getSideNature().inverse() );
}
else {
resultModelPart = keyTargetMatchPart;
}
resultTableGroup = tableGroup;
}
allowFkOptimization = true;
}
else if ( inferredMapping == null && hasNotFound( mapping ) ) {
// This is necessary to allow expression like `where root.notFoundAssociation is null`
// to render to `alias.not_found_fk is null`, but IMO this shouldn't be done
// todo: discuss removing this part and create a left joined table group instead?
resultModelPart = keyTargetMatchPart;
resultTableGroup = sqlAstCreationState.getFromClauseAccess()
.findTableGroup( tableGroup.getNavigablePath().getParent() );
allowFkOptimization = false;
}
else {
// If the mapping is an inverse association, use the PK and disallow FK optimizations
resultModelPart = ( (EntityAssociationMapping) mapping ).getAssociatedEntityMappingType().getIdentifierMapping();
resultTableGroup = tableGroup;
allowFkOptimization = false;
}
}
else {
// If the mapping is not an association, use the PK and disallow FK optimizations
resultModelPart = mapping.getEntityMappingType().getIdentifierMapping();
resultTableGroup = tableGroup;
allowFkOptimization = false;
}
return from(
sqmPath.getNavigablePath(),
tableGroup,
navigablePath,
resultTableGroup,
resultModelPart,
allowFkOptimization,
mapping,
mapping,
sqlAstCreationState
);
}
private static boolean isCorrelated(TableGroup tableGroup, SqmToSqlAstConverter sqlAstCreationState) {
final SqlAstProcessingState processingState = sqlAstCreationState.getCurrentProcessingState();
if ( !( processingState instanceof SqlAstQueryPartProcessingState )
|| ( (SqlAstQueryPartProcessingState) processingState ).getInflightQueryPart().isRoot() ) {
return false;
}
final FromClauseAccess fromClauseAccess = sqlAstCreationState.getFromClauseAccess();
TableGroup realParentTableGroup = fromClauseAccess.findTableGroup( tableGroup.getNavigablePath().getParent() );
while ( realParentTableGroup.getModelPart() instanceof EmbeddableValuedModelPart ) {
realParentTableGroup = fromClauseAccess.findTableGroup( realParentTableGroup.getNavigablePath().getParent() );
}
return realParentTableGroup instanceof CorrelatedTableGroup;
}
private static boolean hasNotFound(EntityValuedModelPart mapping) {
return mapping instanceof ToOneAttributeMapping && ( (ToOneAttributeMapping) mapping ).hasNotFoundAction();
}
private static boolean hasJoinTable(EntityAssociationMapping associationMapping) {
return associationMapping instanceof EntityCollectionPart
&& !( (EntityCollectionPart) associationMapping ).getCollectionDescriptor().isOneToMany();
}
public static <T> EntityValuedPathInterpretation<T> from(
NavigablePath navigablePath,
TableGroup tableGroup,
ModelPart resultModelPart,
boolean allowFkOptimization,
EntityValuedModelPart mapping,
EntityValuedModelPart treatedMapping,
SqmToSqlAstConverter sqlAstCreationState) {
@ -163,7 +317,8 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
final BasicValuedModelPart basicValuedModelPart = (BasicValuedModelPart) resultModelPart;
final TableReference tableReference = tableGroup.resolveTableReference(
navigablePath,
basicValuedModelPart.getContainingTableExpression()
basicValuedModelPart.getContainingTableExpression(),
allowFkOptimization
);
sqlExpression = sqlExprResolver.resolveSqlExpression(
createColumnReferenceKey( tableReference, basicValuedModelPart.getSelectionExpression() ),
@ -180,7 +335,8 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
(selectionIndex, selectableMapping) -> {
final TableReference tableReference = tableGroup.resolveTableReference(
navigablePath,
selectableMapping.getContainingTableExpression()
selectableMapping.getContainingTableExpression(),
allowFkOptimization
);
expressions.add(
sqlExprResolver.resolveSqlExpression(

View File

@ -235,7 +235,8 @@ public class NavigablePath implements DotIdentifierSequence, Serializable {
public String resolve() {
if ( buffer == null ) {
return null;
// Return an empty string instead of null in case the two navigable paths are equal
return matchedBase ? "" : null;
}
return buffer.toString();
}

View File

@ -202,6 +202,17 @@ public class CollectionMapWithComponentValueTest extends BaseCoreFunctionalTestC
} );
}
@Test
@TestForIssue(jiraKey = "HHH-11433")
public void testJoinMapValue() {
doInHibernate( this::sessionFactory, s -> {
// Assert that a left join is used for joining the map key entity table
List keyValues= s.createQuery( "select v from BaseTestEntity bte left join bte.entities te left join te.values v" ).list();
System.out.println( keyValues );
assertEquals( 2, keyValues.size() );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-11433")
public void testJoinMapKey() {

View File

@ -0,0 +1,406 @@
package org.hibernate.orm.test.query;
import java.util.Set;
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.Test;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DomainModel(
annotatedClasses = { CompareEntityValuedPathsTest.Person.class }
)
@SessionFactory(statementInspectorClass = SQLStatementInspector.class)
@TestForIssue(jiraKey = "HHH-15349")
public class CompareEntityValuedPathsTest {
@Test
public void testCompareOneToManyUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select c from p.childrenUk c)" ).list();
// Ensure that there are no joins and we compare by UK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.uk in(" +
"select c1_0.child_uk " +
"from children_uks c1_0 " +
"where p1_0.uk=c1_0.owner_uk" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareOneToManyPK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select c from p.children c)" ).list();
// Ensure that there are no joins and we compare by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.id in(" +
"select c1_0.children_id " +
"from PERSON_TABLE_PERSON_TABLE c1_0 " +
"where p1_0.id=c1_0.Person_id" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testManyToOneIsNull(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p.parent is null" ).list();
// Comparing a PK-ManyToOne against a UK-CollectionPart will force comparing by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.parent_id is null",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testComparePKWithOneToManyUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p.parent in (select c from p.childrenUk c)" ).list();
// Comparing a PK-ManyToOne against a UK-CollectionPart will force comparing by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.parent_id in(" +
"select c1_1.id " +
"from children_uks c1_0 " +
"join PERSON_TABLE c1_1 on c1_1.uk=c1_0.child_uk " +
"where p1_0.uk=c1_0.owner_uk" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareUKWithOneToManyPK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p.parentUk in (select c from p.children c)" ).list();
// Comparing a UK-ManyToOne against a PK-CollectionPart will force comparing by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join PERSON_TABLE p2_0 on p2_0.uk=p1_0.parent_uk " +
"where p2_0.id in(" +
"select c1_0.children_id " +
"from PERSON_TABLE_PERSON_TABLE c1_0 " +
"where p1_0.id=c1_0.Person_id" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testComparePKWithMappedByOneToManyPK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select c from p.employees c)" ).list();
// Comparing a root against a PK-one-to-many CollectionPart allows comparing by FK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.id in(" +
"select e1_0.id " +
"from PERSON_TABLE e1_0 " +
"where p1_0.id=e1_0.supervisor_id" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testComparePKWithMappedByOneToManyUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select c from p.employeesUk c)" ).list();
// Comparing a root against a UK-one-to-many CollectionPart forces comparing by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.id in(" +
"select e1_0.id " +
"from PERSON_TABLE e1_0 " +
"where p1_0.uk=e1_0.supervisor_uk" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testOneToManyUKIsNotNull(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.childrenUk c where c is not null" ).list();
// Assert that we don't join the childrenUk target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join children_uks c1_0 on p1_0.uk=c1_0.owner_uk " +
"where c1_0.child_uk is not null",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testOneToManyPKIsNotNull(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.children c where c is not null" ).list();
// Assert that we don't join the children target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join PERSON_TABLE_PERSON_TABLE c1_0 on p1_0.id=c1_0.Person_id " +
"where c1_0.children_id is not null",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareOneToManyUKWithOneToManyPK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.childrenUk c join p.children c2 where c = c2" ).list();
// Assert that we don't join the childrenUk target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join (children_uks c1_0 join PERSON_TABLE c1_1 on c1_1.uk=c1_0.child_uk) on p1_0.uk=c1_0.owner_uk " +
"join PERSON_TABLE_PERSON_TABLE c2_0 on p1_0.id=c2_0.Person_id " +
"where c1_1.id=c2_0.children_id",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareOneToManyPKWithOneToManyUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.children c join p.childrenUk c2 where c = c2" ).list();
// Assert that we don't join the children target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join PERSON_TABLE_PERSON_TABLE c1_0 on p1_0.id=c1_0.Person_id " +
"join (children_uks c2_0 join PERSON_TABLE c2_1 on c2_1.uk=c2_0.child_uk) on p1_0.uk=c2_0.owner_uk " +
"where c1_0.children_id=c2_1.id",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareOneToManyUKWithSubqueryOneToManyPK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.childrenUk c where c in (select c2 from p.children c2)" ).list();
// Assert that we don't join the childrenUk target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join (children_uks c1_0 join PERSON_TABLE c1_1 on c1_1.uk=c1_0.child_uk) on p1_0.uk=c1_0.owner_uk " +
"where c1_1.id in(select c2_0.children_id from PERSON_TABLE_PERSON_TABLE c2_0 where p1_0.id=c2_0.Person_id)",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareOneToManyPKWithSubqueryOneToManyUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p join p.children c where c in (select c2 from p.childrenUk c2)" ).list();
// Assert that we don't join the children target table, only the join table
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"join PERSON_TABLE_PERSON_TABLE c1_0 on p1_0.id=c1_0.Person_id " +
"where c1_0.children_id in(select c2_1.id from children_uks c2_0 join PERSON_TABLE c2_1 on c2_1.uk=c2_0.child_uk where p1_0.uk=c2_0.owner_uk)",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareManyToOneUK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select p2.parentUk from Person p2)" ).list();
// Ensure that there are no joins and we compare by UK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.uk in(" +
"select p2_0.parent_uk " +
"from PERSON_TABLE p2_0" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Test
public void testCompareManyToOnePK(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
scope.inTransaction(
session -> {
statementInspector.clear();
session.createQuery( "select 1 from Person p where p in (select p2.parent from Person p2)" ).list();
// Ensure that there are no joins and we compare by PK
assertEquals(
"select " +
"1 " +
"from PERSON_TABLE p1_0 " +
"where p1_0.id in(" +
"select p2_0.parent_id " +
"from PERSON_TABLE p2_0" +
")",
statementInspector.getSqlQueries().get( 0 )
);
}
);
}
@Entity(name = "Person")
@Table(name = "PERSON_TABLE")
public static class Person {
@Id
private Long id;
@Column(unique = true)
private String uk;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Person parent;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_uk", referencedColumnName = "uk")
private Person parentUk;
@ManyToOne(fetch = FetchType.LAZY)
private Person supervisor;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "supervisor_uk", referencedColumnName = "uk")
private Person supervisorUk;
@OneToMany
private Set<Person> children;
@OneToMany
@JoinTable(name = "children_uks",
joinColumns = @JoinColumn(name = "owner_uk", referencedColumnName = "uk"),
inverseJoinColumns = @JoinColumn(name = "child_uk", referencedColumnName = "uk")
)
private Set<Person> childrenUk;
@OneToMany(mappedBy = "supervisor")
private Set<Person> employees;
@OneToMany(mappedBy = "supervisorUk")
private Set<Person> employeesUk;
public Person() {
}
}
}

View File

@ -13,6 +13,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.hamcrest.CoreMatchers;
import org.hibernate.annotations.NaturalId;
import org.hibernate.dialect.SybaseDialect;
@ -29,6 +30,7 @@ import org.hibernate.query.sqm.sql.SqmTranslator;
import org.hibernate.query.sqm.sql.SqmTranslatorFactory;
import org.hibernate.query.sqm.tree.SqmStatement;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.sql.ast.tree.from.LazyTableGroup;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.select.SelectStatement;
@ -43,6 +45,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
@ -202,9 +205,14 @@ public class EntityJoinTest {
assertThat( roots.size(), is( 1 ) );
final TableGroup rootTableGroup = roots.get( 0 );
assertThat( rootTableGroup.getTableGroupJoins().size(), is( 1 ) );
assertThat( rootTableGroup.getTableGroupJoins().size(), is( 2 ) );
final TableGroupJoin tableGroupJoin = rootTableGroup.getTableGroupJoins().get( 0 );
// The first table group is an uninitialized lazy table group for the path in the on clause
final TableGroupJoin firstTableGroupJoin = rootTableGroup.getTableGroupJoins().get( 0 );
assertThat( firstTableGroupJoin.getJoinedGroup(), instanceOf( LazyTableGroup.class ) );
assertThat( ((LazyTableGroup) firstTableGroupJoin.getJoinedGroup()).getUnderlyingTableGroup(), is( CoreMatchers.nullValue() ) );
final TableGroupJoin tableGroupJoin = rootTableGroup.getTableGroupJoins().get( 1 );
assertThat( tableGroupJoin.getJoinedGroup().getModelPart(), is( customerEntityDescriptor ) );
}
);