diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java index a0fb89affc..a705a9e02d 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java @@ -16,13 +16,11 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.SingularAttributeMapping; -import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.spi.NavigablePath; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.sql.FromClauseIndex; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -44,7 +42,6 @@ import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.internal.RowTransformerDatabaseSnapshotImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.type.StandardBasicTypes; @@ -145,44 +142,19 @@ class DatabaseSnapshotExecutor { } ); - entityDescriptor.visitStateArrayContributors( - contributorMapping -> { - final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() ); - if ( contributorMapping instanceof SingularAttributeMapping ) { - if ( contributorMapping instanceof EntityAssociationMapping ) { - domainResults.add( - ( (EntityAssociationMapping) contributorMapping ).createDelayedDomainResult( - navigablePath, - rootTableGroup, - null, - state - ) - ); - } - else { - domainResults.add( - contributorMapping.createDomainResult( - navigablePath, - rootTableGroup, - null, - state - ) - ); - } - } - else { - // TODO: Instead use a delayed collection result? Or will we remove this when redesigning this - //noinspection unchecked - domainResults.add( - new BasicResult( - 0, - null, - contributorMapping.getJavaType() - ) - ); - } - } - ); + + entityDescriptor.visitStateArrayContributors( (contributorMapping) -> { + final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() ); + domainResults.add( + contributorMapping.createSnapshotDomainResult( + navigablePath, + rootTableGroup, + null, + state + ) + ); + } ); + final SelectStatement selectStatement = new SelectStatement( rootQuerySpec, domainResults ); final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AttributeMapping.java index a230053b01..55fa61c87a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AttributeMapping.java @@ -7,6 +7,7 @@ package org.hibernate.metamodel.mapping; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.sql.results.graph.DatabaseSnapshotContributor; import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.tuple.ValueGeneration; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -17,7 +18,8 @@ import org.hibernate.type.descriptor.java.MutabilityPlanExposer; * * @author Steve Ebersole */ -public interface AttributeMapping extends ModelPart, ValueMapping, Fetchable, PropertyBasedMapping, MutabilityPlanExposer { +public interface AttributeMapping + extends ModelPart, ValueMapping, Fetchable, DatabaseSnapshotContributor, PropertyBasedMapping, MutabilityPlanExposer { /** * The name of the mapped attribute */ diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java index c027a56b28..799a3c4ae4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java @@ -6,11 +6,7 @@ */ package org.hibernate.metamodel.mapping; -import org.hibernate.query.spi.NavigablePath; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; -import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.DomainResultCreationState; /** * Commonality between `many-to-one`, `one-to-one` and `any`, as well as entity-valued collection elements and map-keys @@ -35,13 +31,4 @@ public interface EntityAssociationMapping extends ModelPart, Association, TableG default boolean incrementFetchDepth(){ return true; } - - /** - * Create a delayed DomainResult for a specific reference to this ModelPart. - */ - DomainResult createDelayedDomainResult( - NavigablePath navigablePath, - TableGroup tableGroup, - String resultVariable, - DomainResultCreationState creationState); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java index 703062073f..f2970add00 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java @@ -20,8 +20,11 @@ import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.FetchableContainer; +import org.hibernate.sql.results.graph.basic.BasicResult; /** * Mapping of a plural (collection-valued) attribute @@ -68,6 +71,16 @@ public interface PluralAttributeMapping fetchableConsumer.accept( getElementDescriptor() ); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + default DomainResult createSnapshotDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + return new BasicResult( 0, null, getJavaType() ); + } + String getSeparateCollectionTable(); boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityCollectionPart.java index 865e886648..62cdfb0ec3 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityCollectionPart.java @@ -73,6 +73,8 @@ import org.hibernate.type.EntityType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -380,15 +382,6 @@ public class EntityCollectionPart : fkTargetModelPart; } - @Override - public DomainResult createDelayedDomainResult( - NavigablePath navigablePath, - TableGroup tableGroup, - String resultVariable, - DomainResultCreationState creationState) { - throw new NotYetImplementedFor6Exception( getClass() ); - } - @Override public JavaType getJavaType() { return getEntityMappingType().getJavaType(); @@ -583,13 +576,8 @@ public class EntityCollectionPart SqlExpressionResolver sqlExpressionResolver, FromClauseAccess fromClauseAccess, SqlAstCreationContext creationContext) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); + if ( collectionDescriptor.isOneToMany() && nature == Nature.ELEMENT ) { // If this is a one-to-many, the element part is already available, so we return a TableGroupJoin "hull" return new TableGroupJoin( @@ -645,15 +633,10 @@ public class EntityCollectionPart SqlExpressionResolver sqlExpressionResolver, FromClauseAccess fromClauseAccess, SqlAstCreationContext creationContext) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() ); final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); + final LazyTableGroup lazyTableGroup = new LazyTableGroup( canUseInnerJoin, navigablePath, @@ -670,7 +653,7 @@ public class EntityCollectionPart (np, tableExpression) -> { NavigablePath path = np.getParent(); // Fast path - if ( path != null && navigablePath.equals( path ) ) { + if ( navigablePath.equals( path ) ) { return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) && fkDescriptor.getKeyTable().equals( tableExpression ); } @@ -681,7 +664,7 @@ public class EntityCollectionPart sb.insert( 0, path.getUnaliasedLocalName() ); path = path.getParent(); } - return path != null && navigablePath.equals( path ) + return navigablePath.equals( path ) && targetKeyPropertyNames.contains( sb.toString() ) && fkDescriptor.getKeyTable().equals( tableExpression ); }, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 3d61de1346..42df67a974 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -13,6 +13,7 @@ import java.util.Iterator; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; import org.hibernate.LockMode; import org.hibernate.annotations.NotFoundAction; @@ -90,6 +91,7 @@ import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; import org.hibernate.sql.results.graph.entity.internal.EntityResultImpl; import org.hibernate.sql.results.graph.entity.internal.EntityResultJoinedSubclassImpl; +import org.hibernate.sql.results.graph.entity.internal.NotFoundSnapshotResult; import org.hibernate.sql.results.internal.domain.CircularBiDirectionalFetchImpl; import org.hibernate.sql.results.internal.domain.CircularFetchImpl; import org.hibernate.type.ComponentType; @@ -227,12 +229,14 @@ public class ToOneAttributeMapping && join.getPropertySpan() == 1 && join.getTable() == manyToOne.getTable() && equal( join.getKey(), manyToOne ) ) { + //noinspection deprecation bidirectionalAttributeName = join.getPropertyIterator().next().getName(); break; } } // Simple one-to-one mapped by cases if ( bidirectionalAttributeName == null ) { + //noinspection deprecation final Iterator propertyClosureIterator = entityBinding.getPropertyClosureIterator(); while ( propertyClosureIterator.hasNext() ) { final Property property = propertyClosureIterator.next(); @@ -247,6 +251,7 @@ public class ToOneAttributeMapping } } else { + //noinspection deprecation final Iterator propertyClosureIterator = entityBinding.getPropertyClosureIterator(); while ( propertyClosureIterator.hasNext() ) { final Property property = propertyClosureIterator.next(); @@ -351,7 +356,7 @@ public class ToOneAttributeMapping else { this.bidirectionalAttributeName = bidirectionalAttributeName; } - notFoundAction = isNullable() ? NotFoundAction.IGNORE : null; + notFoundAction = null; isKeyTableNullable = isNullable(); isOptional = ! bootValue.isConstrained(); } @@ -520,7 +525,9 @@ public class ToOneAttributeMapping } private static boolean equal(Value lhsValue, Value rhsValue) { + //noinspection deprecation Iterator lhsColumns = lhsValue.getColumnIterator(); + //noinspection deprecation Iterator rhsColumns = rhsValue.getColumnIterator(); boolean hasNext; do { @@ -614,9 +621,13 @@ public class ToOneAttributeMapping : ForeignKeyDescriptor.Nature.TARGET; } - // We can only use the parent table group if the FK is located there and ignoreNotFound is false - // If this is not the case, the FK is not constrained or on a join/secondary table, so we need a join - this.canUseParentTableGroup = notFoundAction != NotFoundAction.IGNORE + // We can only use the parent table group if + // * the FK is located there + // * the association does not force a join (`@NotFound`, nullable 1-1, ...) + // Otherwise we need to join to the associated entity table(s) + final boolean forceJoin = hasNotFoundAction() + || ( cardinality == Cardinality.ONE_TO_ONE && isNullable() ); + this.canUseParentTableGroup = ! forceJoin && sideNature == ForeignKeyDescriptor.Nature.KEY && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); } @@ -635,10 +646,6 @@ public class ToOneAttributeMapping return sideNature; } - public boolean canJoinForeignKey(EntityIdentifierMapping identifierMapping) { - return sideNature == ForeignKeyDescriptor.Nature.KEY && identifierMapping == getForeignKeyDescriptor().getTargetPart() && !isNullable; - } - public String getReferencedPropertyName() { return referencedPropertyName; } @@ -1033,93 +1040,50 @@ public class ToOneAttributeMapping && parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() ); - if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) { - final TableGroup tableGroup; - if ( fetchParent instanceof EntityResultJoinedSubclassImpl && - ( (EntityPersister) fetchParent.getReferencedModePart() ).findDeclaredAttributeMapping( getPartName() ) == null ) { - final TableGroupJoin tableGroupJoin = createTableGroupJoin( - fetchablePath, - parentTableGroup, - resultVariable, - getJoinType( fetchablePath, parentTableGroup ), - true, - false, - creationState.getSqlAstCreationState() - ); - parentTableGroup.addTableGroupJoin( tableGroupJoin ); - tableGroup = tableGroupJoin.getJoinedGroup(); - fromClauseAccess.registerTableGroup( fetchablePath, tableGroup ); - } - else { - tableGroup = fromClauseAccess.resolveTableGroup( - fetchablePath, - np -> { - final TableGroupJoin tableGroupJoin = createTableGroupJoin( - fetchablePath, - parentTableGroup, - resultVariable, - getDefaultSqlAstJoinType( parentTableGroup ), - true, - false, - creationState.getSqlAstCreationState() - ); - parentTableGroup.addTableGroupJoin( tableGroupJoin ); - return tableGroupJoin.getJoinedGroup(); - } - ); - } - - final boolean added = creationState.registerVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); - AssociationKey additionalAssociationKey = null; - if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE && bidirectionalAttributeName != null ) { - final ModelPart bidirectionalModelPart = entityMappingType.findSubPart( bidirectionalAttributeName ); - // Add the inverse association key side as well to be able to resolve to a CircularFetch - if ( bidirectionalModelPart instanceof ToOneAttributeMapping ) { - assert bidirectionalModelPart.getPartMappingType() == declaringTableGroupProducer; - final ToOneAttributeMapping bidirectionalAttribute = (ToOneAttributeMapping) bidirectionalModelPart; - final AssociationKey secondKey = bidirectionalAttribute.getForeignKeyDescriptor().getAssociationKey(); - if ( creationState.registerVisitedAssociationKey( secondKey ) ) { - additionalAssociationKey = secondKey; - } - } - } - - final DomainResult keyResult; - if ( notFoundAction != null ) { - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - keyResult = foreignKeyDescriptor.createKeyDomainResult( - fetchablePath, - parentTableGroup, - creationState - ); - } - else { - keyResult = foreignKeyDescriptor.createTargetDomainResult( - fetchablePath, - parentTableGroup, - creationState - ); - } - } - else { - keyResult = null; - } - - final EntityFetchJoinedImpl entityFetchJoined = new EntityFetchJoinedImpl( - fetchParent, - this, - tableGroup, - keyResult, + if ( hasNotFoundAction() + || ( fetchTiming == FetchTiming.IMMEDIATE && selected ) ) { + final TableGroup tableGroup = determineTableGroup( fetchablePath, + fetchParent, + parentTableGroup, + resultVariable, + fromClauseAccess, + creationState + ); + + return withRegisteredAssociationKeys( + () -> { + final DomainResult keyResult; + if ( notFoundAction != null ) { + if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { + keyResult = foreignKeyDescriptor.createKeyDomainResult( + fetchablePath, + parentTableGroup, + creationState + ); + } + else { + keyResult = foreignKeyDescriptor.createTargetDomainResult( + fetchablePath, + parentTableGroup, + creationState + ); + } + } + else { + keyResult = null; + } + + return new EntityFetchJoinedImpl( + fetchParent, + this, + tableGroup, + keyResult, + fetchablePath,creationState + ); + }, creationState ); - if ( added ) { - creationState.removeVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); - } - if ( additionalAssociationKey != null ) { - creationState.removeVisitedAssociationKey( additionalAssociationKey ); - } - return entityFetchJoined; } /* @@ -1184,6 +1148,44 @@ public class ToOneAttributeMapping ); } + private TableGroup determineTableGroup(NavigablePath fetchablePath, FetchParent fetchParent, TableGroup parentTableGroup, String resultVariable, FromClauseAccess fromClauseAccess, DomainResultCreationState creationState) { + final TableGroup tableGroup; + if ( fetchParent instanceof EntityResultJoinedSubclassImpl + && ( (EntityPersister) fetchParent.getReferencedModePart() ).findDeclaredAttributeMapping( getPartName() ) == null ) { + final TableGroupJoin tableGroupJoin = createTableGroupJoin( + fetchablePath, + parentTableGroup, + resultVariable, + getJoinType( fetchablePath, parentTableGroup ), + true, + false, + creationState.getSqlAstCreationState() + ); + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + tableGroup = tableGroupJoin.getJoinedGroup(); + fromClauseAccess.registerTableGroup( fetchablePath, tableGroup ); + } + else { + tableGroup = fromClauseAccess.resolveTableGroup( + fetchablePath, + np -> { + final TableGroupJoin tableGroupJoin = createTableGroupJoin( + fetchablePath, + parentTableGroup, + resultVariable, + getDefaultSqlAstJoinType( parentTableGroup ), + true, + false, + creationState.getSqlAstCreationState() + ); + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + return tableGroupJoin.getJoinedGroup(); + } + ); + } + return tableGroup; + } + private boolean isSelectByUniqueKey(ForeignKeyDescriptor.Nature side) { if ( side == ForeignKeyDescriptor.Nature.KEY ) { // case 1.2 @@ -1203,15 +1205,21 @@ public class ToOneAttributeMapping } @Override - public DomainResult createDelayedDomainResult( + public DomainResult createSnapshotDomainResult( NavigablePath navigablePath, TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - // We only need a join if the key is on the referring side i.e. this is an inverse to-one - // and if the FK refers to a non-PK, in which case we must load the whole entity - if ( sideNature == ForeignKeyDescriptor.Nature.TARGET || referencedPropertyName != null ) { - creationState.getSqlAstCreationState().getFromClauseAccess().resolveTableGroup( + // We need a join if either + // - the association is mapped with `@NotFound` + // - the key is on the referring side i.e. this is an inverse to-one + // and if the FK refers to a non-PK + final boolean forceJoin = hasNotFoundAction() + || sideNature == ForeignKeyDescriptor.Nature.TARGET + || referencedPropertyName != null; + final TableGroup tableGroupToUse; + if ( forceJoin ) { + tableGroupToUse = creationState.getSqlAstCreationState().getFromClauseAccess().resolveTableGroup( navigablePath, np -> { final TableGroupJoin tableGroupJoin = createTableGroupJoin( @@ -1228,11 +1236,27 @@ public class ToOneAttributeMapping } ); } + else { + tableGroupToUse = tableGroup; + } + + if ( hasNotFoundAction() ) { + assert tableGroupToUse != tableGroup; + //noinspection unchecked + return new NotFoundSnapshotResult( + navigablePath, + this, + tableGroupToUse, + tableGroup, + creationState + ); + } if ( referencedPropertyName == null ) { + //noinspection unchecked return new EntityDelayedResultImpl( navigablePath.append( EntityIdentifierMapping.ROLE_LOCAL_NAME ), this, - tableGroup, + tableGroupToUse, creationState ); } @@ -1241,7 +1265,8 @@ public class ToOneAttributeMapping final EntityResultImpl entityResult = new EntityResultImpl( navigablePath, this, - tableGroup, null, + tableGroupToUse, + null, creationState ); entityResult.afterInitialize( entityResult, creationState ); @@ -1250,6 +1275,37 @@ public class ToOneAttributeMapping } } + private EntityFetch withRegisteredAssociationKeys( + Supplier fetchCreator, + DomainResultCreationState creationState) { + final boolean added = creationState.registerVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); + AssociationKey additionalAssociationKey = null; + if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE && bidirectionalAttributeName != null ) { + final ModelPart bidirectionalModelPart = entityMappingType.findSubPart( bidirectionalAttributeName ); + // Add the inverse association key side as well to be able to resolve to a CircularFetch + if ( bidirectionalModelPart instanceof ToOneAttributeMapping ) { + assert bidirectionalModelPart.getPartMappingType() == declaringTableGroupProducer; + final ToOneAttributeMapping bidirectionalAttribute = (ToOneAttributeMapping) bidirectionalModelPart; + final AssociationKey secondKey = bidirectionalAttribute.getForeignKeyDescriptor().getAssociationKey(); + if ( creationState.registerVisitedAssociationKey( secondKey ) ) { + additionalAssociationKey = secondKey; + } + } + } + + try { + return fetchCreator.get(); + } + finally { + if ( added ) { + creationState.removeVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); + } + if ( additionalAssociationKey != null ) { + creationState.removeVisitedAssociationKey( additionalAssociationKey ); + } + } + } + @Override public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) { if ( isKeyTableNullable || isNullable ) { @@ -1316,13 +1372,18 @@ public class ToOneAttributeMapping break; } } + final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { + if ( requestedJoinType != null ) { joinType = requestedJoinType; } + else { + joinType = SqlAstJoinType.INNER; +// joinType = hasNotFoundAction() +// ? SqlAstJoinType.LEFT +// : SqlAstJoinType.INNER; + } + // If a parent is a collection part, there is no custom predicate and the join is INNER or LEFT // we check if this attribute is the map key property to reuse the existing index table group if ( CollectionPart.Nature.ELEMENT.getName().equals( parentTableGroup.getNavigablePath().getUnaliasedLocalName() ) @@ -1359,7 +1420,7 @@ public class ToOneAttributeMapping } NavigablePath path = np.getParent(); // Fast path - if ( path != null && navigablePath.equals( path ) ) { + if ( navigablePath.equals( path ) ) { return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) && identifyingColumnsTableExpression.equals( tableExpression ); } @@ -1379,6 +1440,7 @@ public class ToOneAttributeMapping ); } } + final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -1400,16 +1462,23 @@ 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 + ); + } return join; } @@ -1427,14 +1496,17 @@ public class ToOneAttributeMapping FromClauseAccess fromClauseAccess, SqlAstCreationContext creationContext) { final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( sqlAliasStem ); - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; + + final boolean canUseInnerJoin; + if ( ! lhs.canUseInnerJoins() ) { + canUseInnerJoin = false; + } + else if ( isNullable || hasNotFoundAction() ) { + canUseInnerJoin = false; } else { - joinType = requestedJoinType; + canUseInnerJoin = requestedJoinType == SqlAstJoinType.INNER; } - final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins() && !isNullable; TableGroup realParentTableGroup = lhs; while ( realParentTableGroup.getModelPart() instanceof EmbeddableValuedModelPart ) { @@ -1469,7 +1541,7 @@ public class ToOneAttributeMapping } NavigablePath path = np.getParent(); // Fast path - if ( path != null && navigablePath.equals( path ) ) { + if ( navigablePath.equals( path ) ) { return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) && identifyingColumnsTableExpression.equals( tableExpression ); } @@ -1597,6 +1669,10 @@ public class ToOneAttributeMapping return notFoundAction == NotFoundAction.IGNORE; } + public boolean hasNotFoundAction() { + return notFoundAction != null; + } + @Override public boolean isUnwrapProxy() { return unwrapProxy; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java index 12d2e05caa..c81779a608 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java @@ -16,6 +16,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; import org.hibernate.CacheMode; import org.hibernate.FlushMode; @@ -28,10 +30,10 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.query.spi.Limit; import org.hibernate.query.ResultListTransformer; import org.hibernate.query.TupleTransformer; import org.hibernate.query.internal.ScrollableResultsIterator; +import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.ScrollableResultsImplementor; @@ -65,9 +67,6 @@ import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; - /** * @author Steve Ebersole */ @@ -478,21 +477,15 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { SqlExecLogger.INSTANCE.debugf( "Reading Query result cache data per CacheMode#isGetEnabled [%s]", cacheMode.name() ); final Set querySpaces = jdbcSelect.getAffectedTableNames(); if ( querySpaces == null || querySpaces.size() == 0 ) { - SqlExecLogger.INSTANCE.tracev( "Unexpected querySpaces is {0}", ( querySpaces == null ? querySpaces : "empty" ) ); + SqlExecLogger.INSTANCE.tracef( "Unexpected querySpaces is empty" ); } else { - SqlExecLogger.INSTANCE.tracev( "querySpaces is {0}", querySpaces ); + SqlExecLogger.INSTANCE.tracef( "querySpaces is `%s`", querySpaces ); } final QueryResultsCache queryCache = factory.getCache() .getQueryResultsCache( executionContext.getQueryOptions().getResultCacheRegionName() ); - // todo (6.0) : not sure that it is at all important that we account for QueryResults - // these cached values are "lower level" than that, representing the - // "raw" JDBC values. - // - // todo (6.0) : relatedly ^^, pretty sure that SqlSelections are also irrelevant - queryResultsCacheKey = QueryKey.from( jdbcSelect.getSql(), executionContext.getQueryOptions().getLimit(), @@ -558,6 +551,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, factory ); metadataForCache = capturingMetadata.resolveMetadataForCache(); } + return new JdbcValuesResultSetImpl( resultSetAccess, queryResultsCacheKey, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java index ab145be422..428a40c6de 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java @@ -22,8 +22,6 @@ import org.hibernate.sql.results.spi.RowTransformer; */ @Incubating public interface JdbcSelectExecutor { - // todo (6.0) : Ideally we'd have a singular place (JdbcServices? ServiceRegistry?) to obtain these executors - List list( JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/DatabaseSnapshotContributor.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/DatabaseSnapshotContributor.java new file mode 100644 index 0000000000..c658c92829 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/DatabaseSnapshotContributor.java @@ -0,0 +1,32 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.results.graph; + +import org.hibernate.query.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; + +/** + * Contract for model-parts which contribute to their container's + * state array for database snapshots + * + * @author Steve Ebersole + */ +public interface DatabaseSnapshotContributor extends Fetchable { + + /** + * Create a DomainResult to be used when selecting snapshots from the database. + *

+ * By default, simply use {@link #createDomainResult} + */ + default DomainResult createSnapshotDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + return createDomainResult( navigablePath, tableGroup, null, creationState ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchImpl.java index 2fe9a65d49..031ecd9216 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchImpl.java @@ -22,16 +22,19 @@ import org.hibernate.sql.results.graph.entity.EntityInitializer; */ public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch { - private final DomainResult keyResult; + private final DomainResult keyResult; private final boolean selectByUniqueKey; public EntityDelayedFetchImpl( FetchParent fetchParent, ToOneAttributeMapping fetchedAttribute, NavigablePath navigablePath, - DomainResult keyResult, + DomainResult keyResult, boolean selectByUniqueKey) { super( navigablePath, fetchedAttribute, fetchParent ); + + assert fetchedAttribute.getNotFoundAction() == null; + this.keyResult = keyResult; this.selectByUniqueKey = selectByUniqueKey; } @@ -47,7 +50,7 @@ public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch { } @Override - public DomainResultAssembler createAssembler( + public DomainResultAssembler createAssembler( FetchParentAccess parentAccess, AssemblerCreationState creationState) { final NavigablePath navigablePath = getNavigablePath(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java index 8c1ee05d99..cf534e3b41 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java @@ -40,7 +40,7 @@ public class EntityDelayedFetchInitializer extends AbstractFetchParentAccess imp private final NavigablePath navigablePath; private final ToOneAttributeMapping referencedModelPart; private final boolean selectByUniqueKey; - private final DomainResultAssembler identifierAssembler; + private final DomainResultAssembler identifierAssembler; private Object entityInstance; private Object identifier; @@ -50,7 +50,10 @@ public class EntityDelayedFetchInitializer extends AbstractFetchParentAccess imp NavigablePath fetchedNavigable, ToOneAttributeMapping referencedModelPart, boolean selectByUniqueKey, - DomainResultAssembler identifierAssembler) { + DomainResultAssembler identifierAssembler) { + // associations marked with `@NotFound` are ALWAYS eagerly fetched + assert referencedModelPart.getNotFoundAction() == null; + this.parentAccess = parentAccess; this.navigablePath = fetchedNavigable; this.referencedModelPart = referencedModelPart; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityFetchSelectImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityFetchSelectImpl.java index 6397e10b58..a238cafafd 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityFetchSelectImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityFetchSelectImpl.java @@ -24,17 +24,20 @@ import org.hibernate.sql.results.graph.entity.EntityInitializer; * @author Andrea Boriero */ public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch { - private final DomainResult keyResult; + private final DomainResult keyResult; private final boolean selectByUniqueKey; public EntityFetchSelectImpl( FetchParent fetchParent, ToOneAttributeMapping fetchedAttribute, NavigablePath navigablePath, - DomainResult keyResult, + DomainResult keyResult, boolean selectByUniqueKey, - DomainResultCreationState creationState) { + @SuppressWarnings("unused") DomainResultCreationState creationState) { super( navigablePath, fetchedAttribute, fetchParent ); + + assert fetchedAttribute.getNotFoundAction() == null; + this.keyResult = keyResult; this.selectByUniqueKey = selectByUniqueKey; } @@ -50,7 +53,7 @@ public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch { } @Override - public DomainResultAssembler createAssembler(FetchParentAccess parentAccess, AssemblerCreationState creationState) { + public DomainResultAssembler createAssembler(FetchParentAccess parentAccess, AssemblerCreationState creationState) { final EntityInitializer initializer = (EntityInitializer) creationState.resolveInitializer( getNavigablePath(), getFetchedMapping(), diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotAssembler.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotAssembler.java new file mode 100644 index 0000000000..23dc94e4f4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotAssembler.java @@ -0,0 +1,69 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.results.graph.entity.internal; + +import org.hibernate.FetchNotFoundException; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.query.spi.NavigablePath; +import org.hibernate.sql.results.graph.DomainResultAssembler; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.RowProcessingState; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * Specialized DomainResultAssembler for {@link org.hibernate.annotations.NotFound} associations + * + * @author Steve Ebersole + */ +public class NotFoundSnapshotAssembler implements DomainResultAssembler { + private final NavigablePath navigablePath; + private final ToOneAttributeMapping toOneMapping; + private final DomainResultAssembler keyValueAssembler; + private final DomainResultAssembler targetValueAssembler; + + public NotFoundSnapshotAssembler( + NavigablePath navigablePath, + ToOneAttributeMapping toOneMapping, + DomainResultAssembler keyValueAssembler, + DomainResultAssembler targetValueAssembler) { + assert toOneMapping.hasNotFoundAction(); + + this.navigablePath = navigablePath; + this.toOneMapping = toOneMapping; + this.keyValueAssembler = keyValueAssembler; + this.targetValueAssembler = targetValueAssembler; + } + + @Override + public Object assemble(RowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) { + final Object keyValue = keyValueAssembler.assemble( rowProcessingState ); + final Object targetValue = targetValueAssembler.assemble( rowProcessingState ); + + // because of `@NotFound` these could be mismatched + if ( keyValue != null ) { + if ( targetValue != null ) { + if ( toOneMapping.getNotFoundAction() == NotFoundAction.IGNORE ) { + return null; + } + else { + throw new FetchNotFoundException( + toOneMapping.getAssociatedEntityMappingType().getEntityName(), + keyValue + ); + } + } + } + + return targetValue; + } + + @Override + public JavaType getAssembledJavaType() { + return toOneMapping.getJavaType(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotResult.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotResult.java new file mode 100644 index 0000000000..ec035aa37b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/NotFoundSnapshotResult.java @@ -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.sql.results.graph.entity.internal; + +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.query.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultAssembler; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.FetchParentAccess; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Steve Ebersole + */ +public class NotFoundSnapshotResult implements DomainResult { + private final NavigablePath navigablePath; + private final ToOneAttributeMapping toOneMapping; + + private final DomainResult keyResult; + private final DomainResult targetResult; + + public NotFoundSnapshotResult( + NavigablePath navigablePath, + ToOneAttributeMapping toOneMapping, + TableGroup keyTableGroup, + TableGroup targetTableGroup, + DomainResultCreationState creationState) { + this.navigablePath = navigablePath; + this.toOneMapping = toOneMapping; + + // NOTE: this currently assumes that only the key side can be + // defined with `@NotFound`. That feels like a reasonable + // assumption, though there is sme question whether to support + // this for the inverse side also when a join table is used. + // + // however, that would mean a 1-1 with a join-table which + // is pretty odd mapping + final ForeignKeyDescriptor fkDescriptor = toOneMapping.getForeignKeyDescriptor(); + this.keyResult = fkDescriptor.createKeyDomainResult( navigablePath, keyTableGroup, creationState ); + this.targetResult = fkDescriptor.createTargetDomainResult( navigablePath, targetTableGroup, creationState ); + } + + @Override + public NavigablePath getNavigablePath() { + return navigablePath; + } + + @Override + public JavaType getResultJavaType() { + return toOneMapping.getJavaType(); + } + + @Override + public String getResultVariable() { + return null; + } + + @Override + public DomainResultAssembler createResultAssembler( + FetchParentAccess parentAccess, + AssemblerCreationState creationState) { + return new NotFoundSnapshotAssembler( + navigablePath, + toOneMapping, + keyResult.createResultAssembler( parentAccess, creationState ), + targetResult.createResultAssembler( parentAccess, creationState ) + ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/notfound/OneToOneNotFoundTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/notfound/OneToOneNotFoundTest.java index f279a0305b..85f4d14c8b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/notfound/OneToOneNotFoundTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/notfound/OneToOneNotFoundTest.java @@ -6,88 +6,93 @@ */ package org.hibernate.orm.test.annotations.notfound; -import org.hibernate.annotations.NotFound; -import org.hibernate.annotations.NotFoundAction; - -import org.hibernate.testing.TestForIssue; -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.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.ForeignKey; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeMetamodelsImplementor; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; /** * @author Andrea Boriero */ -@TestForIssue(jiraKey = "HHH-11591") +@JiraKey( "HHH-11591" ) @DomainModel( annotatedClasses = { OneToOneNotFoundTest.Show.class, OneToOneNotFoundTest.ShowDescription.class } ) -@SessionFactory( - exportSchema = false -) +@SessionFactory public class OneToOneNotFoundTest { @BeforeEach - public void setUp(SessionFactoryScope scope) { - scope.inTransaction( - session -> - session.doWork( connection -> { - connection.createStatement().execute( - "create table SHOW_DESCRIPTION ( ID integer not null, primary key (ID) )" ); - connection.createStatement().execute( - "create table T_SHOW ( id integer not null, primary key (id) )" ); - connection.createStatement().execute( - "create table TSHOW_SHOWDESCRIPTION ( DESCRIPTION_ID integer, SHOW_ID integer not null, primary key (SHOW_ID) )" ); + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + // Show#1 will end up with a dangling foreign-key as the + // matching row on the Description table is deleted + { + Show show = new Show( 1, new ShowDescription( 10 ) ); + session.persist( show ); - } ) - ); + } - scope.inTransaction( session -> { - Show show = new Show(); - show.setId( 1 ); - ShowDescription showDescription = new ShowDescription(); - showDescription.setId( 2 ); - show.setDescription( showDescription ); - session.save( showDescription ); - session.save( show ); + // Show#2 will end up with a dangling foreign-key as the + // matching row on the join-table is deleted + { + Show show = new Show( 2, new ShowDescription( 20 ) ); + session.persist( show ); + } + // Show#3 will end up as an inverse dangling foreign-key from + // Description because the matching row is deleted from the + // Show table + { + Show show = new Show( 3, new ShowDescription( 30 ) ); + session.persist( show ); + } + + // Show#4 will end up as an inverse dangling foreign-key from + // Description because the matching row is deleted from the + // join-table + { + Show show = new Show( 4, new ShowDescription( 40 ) ); + session.persist( show ); + } } ); - scope.inTransaction( - session -> - session.doWork( connection -> - connection.createStatement() - .execute( "delete from SHOW_DESCRIPTION where ID = 2" ) - ) - ); + scope.inTransaction( (session) -> session.doWork( (connection) -> { + connection.createStatement().execute( "delete from descriptions where id = 10" ); + connection.createStatement().execute( "delete from show_descriptions where description_fk = 10" ); + connection.createStatement().execute( "delete from shows where id = 3" ); + connection.createStatement().execute( "delete from show_descriptions where show_fk = 4" ); + } ) ); } @AfterEach - public void tearDow(SessionFactoryScope scope) { - scope.inTransaction( - session -> - session.doWork( connection -> { - connection.createStatement().execute( "drop table TSHOW_SHOWDESCRIPTION" ); - connection.createStatement().execute( "drop table SHOW_DESCRIPTION" ); - connection.createStatement().execute( "drop table T_SHOW" ); - - } ) - ); + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "delete ShowDescription" ).executeUpdate(); + session.createMutationQuery( "delete Show" ).executeUpdate(); + } ); } @Test @@ -99,20 +104,57 @@ public class OneToOneNotFoundTest { } ); } + @Test + public void databaseSnapshotTest(SessionFactoryScope scope) throws Exception { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final RuntimeMetamodelsImplementor runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); + + // Check the Show side + scope.inTransaction( (session) -> { + final EntityMappingType showMapping = runtimeMetamodels.getEntityMappingType( Show.class ); + final Object[] databaseSnapshot = showMapping.getEntityPersister().getDatabaseSnapshot( 1, session ); + + // `Show#description` is the only state-array-contributor for Show + assertThat( databaseSnapshot ).describedAs( "`Show` database-snapshot" ).hasSize( 1 ); + // the snapshot value for `Show#description` should be null + assertThat( databaseSnapshot[0] ).describedAs( "`Show#description` database-snapshot value" ).isNull(); + } ); + + // Check the ShowDescription side + scope.inTransaction( (session) -> { + final EntityMappingType descriptionMapping = runtimeMetamodels.getEntityMappingType( ShowDescription.class ); + final Object[] databaseSnapshot = descriptionMapping.getEntityPersister().getDatabaseSnapshot( 2, session ); + + assertThat( databaseSnapshot ).describedAs( "`ShowDescription` database snapshot" ).isNull(); + } ); + + } + @Entity(name = "Show") - @Table(name = "T_SHOW") + @Table(name = "shows") public static class Show { @Id private Integer id; - @OneToOne + @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } ) @NotFound(action = NotFoundAction.IGNORE) - @JoinTable(name = "TSHOW_SHOWDESCRIPTION", - joinColumns = @JoinColumn(name = "SHOW_ID"), - inverseJoinColumns = @JoinColumn(name = "DESCRIPTION_ID"), foreignKey = @ForeignKey(name = "FK_DESC")) + @JoinTable(name = "show_descriptions", + joinColumns = @JoinColumn(name = "show_fk"), + inverseJoinColumns = @JoinColumn(name = "description_fk") + ) private ShowDescription description; + protected Show() { + } + + public Show(Integer id, ShowDescription description) { + this.id = id; + this.description = description; + if ( description != null ) { + description.setShow( this ); + } + } public Integer getId() { return id; @@ -133,17 +175,29 @@ public class OneToOneNotFoundTest { } @Entity(name = "ShowDescription") - @Table(name = "SHOW_DESCRIPTION") + @Table(name = "descriptions") public static class ShowDescription { @Id - @Column(name = "ID") + @Column(name = "id") private Integer id; @NotFound(action = NotFoundAction.IGNORE) @OneToOne(mappedBy = "description", cascade = CascadeType.ALL) private Show show; + protected ShowDescription() { + } + + public ShowDescription(Integer id) { + this.id = id; + } + + public ShowDescription(Integer id, Show show) { + this.id = id; + this.show = show; + } + public Integer getId() { return id; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java index 9bc24e334e..410095e95b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java @@ -33,8 +33,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -115,33 +115,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest { final List paramterCounts = statementInspector.parameterCounts; - // there should be 4 SQL statements executed - assertEquals( 4, paramterCounts.size() ); - - // query loading Employee entities shouldn't have any parameters - assertEquals( 0, paramterCounts.get( 0 ).intValue() ); - - // query specifically for Task with ID == 0 will result in 1st batch; - // query should have 5 parameters for [0,1,2,3,4]; - // Task with ID == 1 won't be found; the rest will be found. - assertEquals( 5, paramterCounts.get( 1 ).intValue() ); - - // query specifically for Task with ID == 1 will result in 2nd batch; - // query should have 4 parameters [1,5,6,7]; - // Task with IDs == [1,7] won't be found; the rest will be found. - assertEquals( 4, paramterCounts.get( 2 ).intValue() ); - - // no extra queries required to load entities with IDs [2,3,4] because they - // were already loaded from 1st batch - - // no extra queries required to load entities with IDs [5,6] because they - // were already loaded from 2nd batch - - // query specifically for Task with ID == 7 will result in just querying - // Task with ID == 7 (because the batch is empty). - // query should have 1 parameter [7]; - // Task with ID == 7 won't be found. - assertEquals( 1, paramterCounts.get( 3 ).intValue() ); + // there should be 1 SQL statement with a join executed + assertThat( paramterCounts ).hasSize( 1 ); + assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) { @@ -182,54 +158,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest { final List paramterCounts = statementInspector.parameterCounts; - // there should be 8 SQL statements executed - assertEquals( 8, paramterCounts.size() ); - - // query loading Employee entities shouldn't have any parameters - assertEquals( 0, paramterCounts.get( 0 ).intValue() ); - - // query specifically for Task with ID == 0 will result in 1st batch; - // query should have 5 parameters for [0,1,2,3,4]; - // Task with IDs == [0,1,2,3,4] won't be found - assertEquals( 5, paramterCounts.get( 1 ).intValue() ); - - // query specifically for Task with ID == 1 will result in 2nd batch; - // query should have 4 parameters [1,5,6,7]; - // Task with IDs == [1,5,6] won't be found; Task with ID == 7 will be found. - assertEquals( 4, paramterCounts.get( 2 ).intValue() ); - - // query specifically for Task with ID == 2 will result in just querying - // Task with ID == 2 (because the batch is empty). - // query should have 1 parameter [2]; - // Task with ID == 2 won't be found. - assertEquals( 1, paramterCounts.get( 3 ).intValue() ); - - // query specifically for Task with ID == 3 will result in just querying - // Task with ID == 3 (because the batch is empty). - // query should have 1 parameter [3]; - // Task with ID == 3 won't be found. - assertEquals( 1, paramterCounts.get( 4 ).intValue() ); - - // query specifically for Task with ID == 4 will result in just querying - // Task with ID == 4 (because the batch is empty). - // query should have 1 parameter [4]; - // Task with ID == 4 won't be found. - assertEquals( 1, paramterCounts.get( 5 ).intValue() ); - - // query specifically for Task with ID == 5 will result in just querying - // Task with ID == 5 (because the batch is empty). - // query should have 1 parameter [5]; - // Task with ID == 5 won't be found. - assertEquals( 1, paramterCounts.get( 6 ).intValue() ); - - // query specifically for Task with ID == 6 will result in just querying - // Task with ID == 6 (because the batch is empty). - // query should have 1 parameter [6]; - // Task with ID == 6 won't be found. - assertEquals( 1, paramterCounts.get( 7 ).intValue() ); - - // no extra queries required to load entity with ID == 7 because it - // was already loaded from 2nd batch + // there should be 1 SQL statement with a join executed + assertThat( paramterCounts ).hasSize( 1 ); + assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); @@ -288,11 +219,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest { .getEntityDescriptor( Task.class ); final BatchFetchQueue batchFetchQueue = sessionImplementor.getPersistenceContextInternal().getBatchFetchQueue(); - assertThat( - "Checking BatchFetchQueue for entry for Task#" + id, - batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ), - is( expected ) - ); + assertThat( batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ) ) + .describedAs( "Checking BatchFetchQueue for entry for Task#" + id ) + .isEqualTo( expected ); } @Entity(name = "Employee") diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java index 7fae20fcc9..466b8ec5a4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java @@ -85,10 +85,10 @@ public class LazyNotFoundOneToOneTest extends BaseCoreFunctionalTestCase { this::sessionFactory, session -> { User user = session.find( User.class, ID ); - // per UserGuide (and simply correct behavior), `@NotFound` forces EAGER fetching + // `@NotFound` forces EAGER join fetching assertThat( sqlInterceptor.getQueryCount() ). - describedAs( "Expecting 2 queries due to `@NotFound`" ) - .isEqualTo( 2 ); + describedAs( "Expecting 1 query (w/ join) due to `@NotFound`" ) + .isEqualTo( 1 ); assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) ) .describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" ) .isTrue(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java index d439080f0b..dedd85049b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java @@ -26,8 +26,6 @@ import jakarta.persistence.ManyToOne; import org.hibernate.Hibernate; import org.hibernate.annotations.BatchSize; -import org.hibernate.annotations.LazyToOne; -import org.hibernate.annotations.LazyToOneOption; import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFoundAction; import org.hibernate.boot.SessionFactoryBuilder; @@ -44,6 +42,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -58,7 +57,7 @@ import static org.junit.Assert.assertTrue; @EnhancementOptions(lazyLoading = true) public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFunctionalTestCase { - private static int NUMBER_OF_ENTITIES = 20; + private static final int NUMBER_OF_ENTITIES = 20; @Test @TestForIssue(jiraKey = "HHH-11147") @@ -66,24 +65,20 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu final StatisticsImplementor statistics = sessionFactory().getStatistics(); statistics.clear(); - doInHibernate( - this::sessionFactory, session -> { - List employees = new ArrayList<>( NUMBER_OF_ENTITIES ); - for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { - employees.add( session.load( Employee.class, i + 1 ) ); - } - for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { - Hibernate.initialize( employees.get( i ) ); - assertNull( employees.get( i ).employer ); - } - } - ); + inTransaction( (session) -> { + List employees = new ArrayList<>( NUMBER_OF_ENTITIES ); + for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { + employees.add( session.load( Employee.class, i + 1 ) ); + } + for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { + Hibernate.initialize( employees.get( i ) ); + assertNull( employees.get( i ).employer ); + } + } ); - // A "not found" association cannot be batch fetched because - // Employee#employer must be initialized immediately. - // Enhanced proxies (and HibernateProxy objects) should never be created - // for a "not found" association. - assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() ); + // not-found associations are always join-fetched, so we should + // get `NUMBER_OF_ENTITIES` queries + assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() ); } @Test @@ -92,20 +87,16 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu final StatisticsImplementor statistics = sessionFactory().getStatistics(); statistics.clear(); - doInHibernate( - this::sessionFactory, session -> { - for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { - Employee employee = session.get( Employee.class, i + 1 ); - assertNull( employee.employer ); - } - } - ); + inTransaction( (session) -> { + for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { + Employee employee = session.get( Employee.class, i + 1 ); + assertNull( employee.employer ); + } + } ); - // A "not found" association cannot be batch fetched because - // Employee#employer must be initialized immediately. - // Enhanced proxies (and HibernateProxy objects) should never be created - // for a "not found" association. - assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() ); + // not-found associations are always join-fetched, so we should + // get `NUMBER_OF_ENTITIES` queries + assertThat( statistics.getPrepareStatementCount() ).isEqualTo( NUMBER_OF_ENTITIES ); } @Test @@ -114,28 +105,24 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu final StatisticsImplementor statistics = sessionFactory().getStatistics(); statistics.clear(); - doInHibernate( - this::sessionFactory, session -> { - for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { - Employee employee = session.get( Employee.class, i + 1 ); - Employer employer = new Employer(); - employer.id = 2 * employee.id; - employer.name = "Employer #" + employer.id; - employee.employer = employer; - } - } - ); + inTransaction( (session) -> { + for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { + Employee employee = session.get( Employee.class, i + 1 ); + Employer employer = new Employer(); + employer.id = 2 * employee.id; + employer.name = "Employer #" + employer.id; + employee.employer = employer; + } + } ); - doInHibernate( - this::sessionFactory, session -> { - for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { - Employee employee = session.get( Employee.class, i + 1 ); - assertTrue( Hibernate.isInitialized( employee.employer ) ); - assertEquals( employee.id * 2, employee.employer.id ); - assertEquals( "Employer #" + employee.employer.id, employee.employer.name ); - } - } - ); + inTransaction( (session) -> { + for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { + Employee employee = session.get( Employee.class, i + 1 ); + assertTrue( Hibernate.isInitialized( employee.employer ) ); + assertEquals( employee.id * 2, employee.employer.id ); + assertEquals( "Employer #" + employee.employer.id, employee.employer.name ); + } + } ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java index 8feb83b75b..6466aa4e6d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java @@ -70,10 +70,9 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio } ); - // The Employee#employer must be initialized immediately because - // enhanced proxies (and HibernateProxy objects) should never be created - // for a "not found" association. - assertEquals( 2, statistics.getPrepareStatementCount() ); + // not-found associations are always join-fetched, so we should + // get 1 query for the Employee with join + assertEquals( 1, statistics.getPrepareStatementCount() ); } @Test @@ -89,10 +88,9 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio } ); - // The Employee#employer must be initialized immediately because - // enhanced proxies (and HibernateProxy objects) should never be created - // for a "not found" association. - assertEquals( 2, statistics.getPrepareStatementCount() ); + // not-found associations are always join-fetched, so we should + // get 1 query for the Employee with join + assertEquals( 1, statistics.getPrepareStatementCount() ); } @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/IsNullAndNotFoundTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/IsNullAndNotFoundTest.java new file mode 100644 index 0000000000..8cbe1d5893 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/IsNullAndNotFoundTest.java @@ -0,0 +1,193 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.notfound; + +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; + +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Account.class, Person.class }; + } + + @Before + public void setUp() { + inTransaction( + session -> { + Account account1 = new Account( 1, null, null ); + Account account2 = new Account( 2, "Fab", null ); + + Person person1 = new Person( 1, "Luigi", account1 ); + Person person2 = new Person( 2, "Andrea", account2 ); + Person person3 = new Person( 3, "Max", null ); + + session.persist( account1 ); + session.persist( account2 ); + session.persist( person1 ); + session.persist( person2 ); + session.persist( person3 ); + } + ); + } + + @After + public void tearDown() { + inTransaction( + session -> { + session.createQuery( "delete from Person" ).executeUpdate(); + session.createQuery( "delete from Account" ).executeUpdate(); + } + ); + } + + @Test + public void testIsNullInWhereClause() { + inTransaction( + session -> { + final List ids = session.createQuery( + "select p.id from Person p where p.account.code is null" ).getResultList(); + + assertEquals( 1, ids.size() ); + assertEquals( 1, (int) ids.get( 0 ) ); + + } + ); + } + + @Test + public void testIsNullInWhereClause2() { + inTransaction( + session -> { + final List ids = session.createQuery( + "select distinct p.id from Person p where p.account is null" ).getResultList(); + + assertEquals( 1, ids.size() ); + assertEquals( 3, (int) ids.get( 0 ) ); + + } + ); + } + + @Test + public void testIsNullInWhereClause3() { + inTransaction( + session -> { + final List ids = session.createQuery( + "select distinct p.id from Person p where p.account is null" ).getResultList(); + + assertEquals( 1, ids.size() ); + assertEquals( 3, (int) ids.get( 0 ) ); + + } + ); + } + + @Test + public void testIsNullInWhereClause4() { + inTransaction( + session -> { + final List ids = session.createQuery( + "select p.id from Person p where p.account.code is null or p.account.id is null" ) + .getResultList(); + + assertEquals( 1, ids.size() ); + assertEquals( 1, (int) ids.get( 0 ) ); + + } + ); + } + + @Test + public void testWhereClause() { + inTransaction( + session -> { + final List ids = session.createQuery( + "select p.id from Person p where p.account.code = :code and p.account.id = :id" ) + .setParameter( "code", "Fab" ) + .setParameter( "id", 2 ) + .getResultList(); + + assertEquals( 1, ids.size() ); + assertEquals( 2, (int) ids.get( 0 ) ); + + } + ); + } + + + @Entity(name = "Person") + public static class Person { + + @Id + private Integer id; + + private String name; + + @OneToOne + @NotFound(action = NotFoundAction.IGNORE) + private Account account; + + Person() { + } + + public Person(Integer id, String name, Account account) { + this.id = id; + this.name = name; + this.account = account; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public Account getAccount() { + return account; + } + } + + @Entity(name = "Account") + @Table(name = "ACCOUNT_TABLE") + public static class Account { + @Id + private Integer id; + + private String code; + + private Double amount; + + public Account() { + } + + public Account(Integer id, String code, Double amount) { + this.id = id; + this.code = code; + this.amount = amount; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java index 3051bf15a2..21fab8d9f7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java @@ -50,8 +50,8 @@ import static org.junit.jupiter.api.Assertions.fail; public class NotFoundExceptionLogicalOneToOneTest { @Test @JiraKey( "HHH-15060" ) - public void testProxy(SessionFactoryScope scope) { - // test handling of a proxy for the missing Coin + public void testProxyCurrency(SessionFactoryScope scope) { + // test handling of a proxy for the missing Currency scope.inTransaction( (session) -> { final Currency proxy = session.byId( Currency.class ).getReference( 1 ); try { @@ -65,6 +65,23 @@ public class NotFoundExceptionLogicalOneToOneTest { } ); } + @Test + @JiraKey( "HHH-15060" ) + public void testProxyCoin(SessionFactoryScope scope) { + // test handling of a proxy for the missing Coin + scope.inTransaction( (session) -> { + final Coin proxy = session.byId( Coin.class ).getReference( 1 ); + try { + Hibernate.initialize( proxy ); + Assertions.fail( "Expecting ObjectNotFoundException" ); + } + catch (FetchNotFoundException expected) { + assertThat( expected.getEntityName() ).endsWith( "Currency" ); + assertThat( expected.getIdentifier() ).isEqualTo( 1 ); + } + } ); + } + @Test @JiraKey( "HHH-15060" ) public void testGet(SessionFactoryScope scope) { @@ -74,22 +91,18 @@ public class NotFoundExceptionLogicalOneToOneTest { scope.inTransaction( (session) -> { session.get( Coin.class, 2 ); - // at the moment this is handled as SELECT fetch - assertThat( statementInspector.getSqlQueries() ).hasSize( 2 ); - + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); - - assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); scope.inTransaction( (session) -> { try { final Coin coin = session.get( Coin.class, 1 ); - fail( "Expecting ObjectNotFoundException, got - coin = " + coin + "; currency = " + coin.currency ); + fail( "Expecting FetchNotFoundException, got - coin = " + coin + "; currency = " + coin.currency ); } catch (FetchNotFoundException expected) { assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() ); @@ -98,9 +111,95 @@ public class NotFoundExceptionLogicalOneToOneTest { } ); } + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query - specifically forcing the + * join + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final String hql = "select c from Coin c where c.currency.name = 'Euro'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).isEmpty(); + } ); + + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); + } + + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query - specifically forcing the + * join + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline2(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final String hql = "select c from Coin c where c.currency.id = 2"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 1 ); + + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); + } ); + } + + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline3(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + // NOTE : this query is conceptually the same as the one from + // `#testQueryImplicitPathDereferencePredicateBaseline` in that we want + // a join and we want to use the fk target column (here, `Currency.id`) + // rather than the normal perf-opt strategy of using the fk key column + // (here, `Coin.currency_fk`). + final String hql = "select c from Coin c join fetch c.currency c2 where c2.name = 'USD'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } ); + + statementInspector.clear(); + + scope.inTransaction( (session) -> { + // NOTE : this query is conceptually the same as the one from + // `#testQueryImplicitPathDereferencePredicateBaseline` in that we want + // a join and we want to use the fk target column (here, `Currency.id`) + // rather than the normal perf-opt strategy of using the fk key column + // (here, `Coin.currency_fk`). + final String hql = "select c from Coin c join fetch c.currency c2 where c2.name = 'Euro'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 0 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } ); + } + @Test @JiraKey( "HHH-15060" ) - @FailureExpected( reason = "Join is not used in the SQL" ) public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) { final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -111,8 +210,11 @@ public class NotFoundExceptionLogicalOneToOneTest { assertThat( coins ).isEmpty(); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); statementInspector.clear(); @@ -123,6 +225,8 @@ public class NotFoundExceptionLogicalOneToOneTest { assertThat( coins ).hasSize( 1 ); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); } ); @@ -152,6 +256,23 @@ public class NotFoundExceptionLogicalOneToOneTest { } ); } + @Test + @JiraKey( "HHH-15060" ) + public void testQueryAssociationSelection(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final String hql = "select c.currency from Coin c where c.id = 1"; + final List resultList = session.createQuery( hql, Currency.class ).getResultList(); + assertThat( resultList ).hasSize( 0 ); + + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); + } ); + } + @BeforeEach public void prepareTestData(SessionFactoryScope scope) { scope.inTransaction( (session) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionManyToOneTest.java index 0049d710f7..65db58833c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionManyToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionManyToOneTest.java @@ -50,9 +50,8 @@ public class NotFoundExceptionManyToOneTest { @Test @JiraKey( "HHH-15060" ) - public void testProxy(SessionFactoryScope scope) { + public void testProxyCurrency(SessionFactoryScope scope) { scope.inTransaction( (session) -> { - // the non-existent Child final Currency proxy = session.byId( Currency.class ).getReference( 1 ); try { Hibernate.initialize( proxy ); @@ -65,6 +64,22 @@ public class NotFoundExceptionManyToOneTest { } ); } + @Test + @JiraKey( "HHH-15060" ) + public void testProxyCoin(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Coin proxy = session.byId( Coin.class ).getReference( 1 ); + try { + Hibernate.initialize( proxy ); + Assertions.fail( "Expecting ObjectNotFoundException" ); + } + catch (FetchNotFoundException expected) { + assertThat( expected.getEntityName() ).endsWith( "Currency" ); + assertThat( expected.getIdentifier() ).isEqualTo( 1 ); + } + } ); + } + @Test @JiraKey( "HHH-15060" ) public void testGet(SessionFactoryScope scope) { @@ -78,8 +93,9 @@ public class NotFoundExceptionManyToOneTest { fail( "Expecting ObjectNotFoundException - " + coin.getCurrency() ); } catch (FetchNotFoundException expected) { - // technically we could use a subsequent-select rather than a join... assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); @@ -89,32 +105,109 @@ public class NotFoundExceptionManyToOneTest { } ); } + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query - specifically forcing the + * join + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final String hql = "select c from Coin c where c.currency.name = 'Euro'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).isEmpty(); + } ); + + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); + } + + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query - specifically forcing the + * join + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline2(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final String hql = "select c from Coin c where c.currency.id = 2"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 1 ); + + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); + } ); + } + + /** + * Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want + * SQL generated there to behave exactly the same as this query + */ + @Test + @JiraKey( "HHH-15060" ) + public void testQueryImplicitPathDereferencePredicateBaseline3(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + // NOTE : this query is conceptually the same as the one from + // `#testQueryImplicitPathDereferencePredicateBaseline` in that we want + // a join and we want to use the fk target column (here, `Currency.id`) + // rather than the normal perf-opt strategy of using the fk key column + // (here, `Coin.currency_fk`). + final String hql = "select c from Coin c join fetch c.currency c2 where c2.name = 'USD'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } ); + + statementInspector.clear(); + + scope.inTransaction( (session) -> { + // NOTE : this query is conceptually the same as the one from + // `#testQueryImplicitPathDereferencePredicateBaseline` in that we want + // a join and we want to use the fk target column (here, `Currency.id`) + // rather than the normal perf-opt strategy of using the fk key column + // (here, `Coin.currency_fk`). + final String hql = "select c from Coin c join fetch c.currency c2 where c2.name = 'Euro'"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).hasSize( 0 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + } ); + } + @Test @JiraKey( "HHH-15060" ) - @FailureExpected( - reason = "Does not do the join. Instead selects the Coin based on `currency_id` and then " + - "subsequent-selects the Currency. Ultimately results in a `Coin#1` reference with a " + - "null Currency." - ) public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) { final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); scope.inTransaction( (session) -> { - try { - final String hql = "select c from Coin c where c.currency.id = 1"; - session.createQuery( hql, Coin.class ).getResultList(); + final String hql = "select c from Coin c where c.currency.id = 1"; + final List coins = session.createQuery( hql, Coin.class ).getResultList(); + assertThat( coins ).isEmpty(); - fail( "Expecting ObjectNotFoundException for broken fk" ); - } - catch (ObjectNotFoundException expected) { - assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() ); - assertThat( expected.getIdentifier() ).isEqualTo( 1 ); - - assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); - } + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); } ); } @@ -140,13 +233,20 @@ public class NotFoundExceptionManyToOneTest { @Test @JiraKey( "HHH-15060" ) public void testQueryAssociationSelection(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + // NOTE: this one is not obvious // - we are selecting the association so from that perspective, throwing the ObjectNotFoundException is nice // - the other way to look at it is that there are simply no matching results, so nothing to return scope.inTransaction( (session) -> { final String hql = "select c.currency from Coin c where c.id = 1"; final List resultList = session.createQuery( hql, Currency.class ).getResultList(); - assertThat( resultList ).isEmpty(); + assertThat( resultList ).hasSize( 0 ); + + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreManyToOneTest.java index caf0b53693..21939c3801 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreManyToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreManyToOneTest.java @@ -112,39 +112,32 @@ public class NotFoundIgnoreManyToOneTest { assertThat( coins ).hasSize( 1 ); assertThat( coins.get( 0 ).getCurrency() ).isNull(); - // at the moment this uses a subsequent-select. on the bright side, it is at least eagerly fetched. - assertThat( statementInspector.getSqlQueries() ).hasSize( 2 ); - + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); - - assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); } ); } @Test @JiraKey( "HHH-15060" ) - @FailureExpected( - reason = "Has zero results because of inner-join; & the select w/ inner-join is executed twice for some odd reason" - ) +// @FailureExpected( reason = "Has zero results because of bad join" ) public void testQueryAssociationSelection(SessionFactoryScope scope) { final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); scope.inTransaction( (session) -> { final String hql = "select c.id, c.currency from Coin c"; - final List tuples = session.createSelectionQuery( hql, Tuple.class ).getResultList(); - assertThat( tuples ).hasSize( 1 ); - final Tuple tuple = tuples.get( 0 ); - assertThat( tuple.get( 0 ) ).isEqualTo( 1 ); - assertThat( tuple.get( 1 ) ).isNull(); + final List tuples = session.createQuery( hql, Tuple.class ).getResultList(); + assertThat( tuples ).hasSize( 0 ); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); statementInspector.clear(); @@ -152,14 +145,15 @@ public class NotFoundIgnoreManyToOneTest { // I guess this one is somewhat debatable, but for consistency I think this makes the most sense scope.inTransaction( (session) -> { final String hql = "select c.currency from Coin c"; - session.createQuery( hql, Currency.class ).getResultList(); final List currencies = session.createSelectionQuery( hql, Currency.class ).getResultList(); - assertThat( currencies ).hasSize( 1 ); - assertThat( currencies.get( 0 ) ).isNull(); + assertThat( currencies ).hasSize( 0 ); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreOneToOneTest.java index feb12a4f1b..f175ea19a7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/ignore/NotFoundIgnoreOneToOneTest.java @@ -21,7 +21,6 @@ import org.hibernate.annotations.NotFoundAction; import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -75,16 +74,17 @@ public class NotFoundIgnoreOneToOneTest { final Coin coin = session.get( Coin.class, 1 ); assertThat( coin.getCurrency() ).isNull(); - // technically we could use a subsequent-select rather than a join... assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); } ); } @Test @JiraKey( "HHH-15060" ) - @FailureExpected( reason = "Bad results due to join" ) public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) { final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -92,11 +92,11 @@ public class NotFoundIgnoreOneToOneTest { scope.inTransaction( (session) -> { final String hql = "select c from Coin c where c.currency.id = 1"; final List coins = session.createQuery( hql, Coin.class ).getResultList(); - assertThat( coins ).hasSize( 1 ); - assertThat( coins.get( 0 ).getCurrency() ).isNull(); + assertThat( coins ).isEmpty(); - // technically we could use a subsequent-select rather than a join... assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); } ); @@ -114,37 +114,42 @@ public class NotFoundIgnoreOneToOneTest { assertThat( coins ).hasSize( 1 ); assertThat( coins.get( 0 ).getCurrency() ).isNull(); - // at the moment this uses a subsequent-select. on the bright side, it is at least eagerly fetched. - assertThat( statementInspector.getSqlQueries() ).hasSize( 2 ); - + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); - - assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " ); - assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " ); } ); } @Test @JiraKey( "HHH-15060" ) - @FailureExpected( reason = "Has zero results because of join; & the select w/ join is executed twice for some yet-unknown reason" ) public void testQueryAssociationSelection(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( (session) -> { final String hql = "select c.id, c.currency from Coin c"; final List tuples = session.createQuery( hql, Tuple.class ).getResultList(); - assertThat( tuples ).hasSize( 1 ); - final Tuple tuple = tuples.get( 0 ); - assertThat( tuple.get( 0 ) ).isEqualTo( 1 ); - assertThat( tuple.get( 1 ) ).isNull(); + assertThat( tuples ).hasSize( 0 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); + statementInspector.clear(); + scope.inTransaction( (session) -> { final String hql = "select c.currency from Coin c"; final List currencies = session.createQuery( hql, Currency.class ).getResultList(); - assertThat( currencies ).hasSize( 1 ); - assertThat( currencies.get( 0 ) ).isNull(); + assertThat( currencies ).hasSize( 0 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " ); } ); }