HHH-15099 - Improve handling of associations marked with @NotFound

- database snapshot handling
This commit is contained in:
Steve Ebersole 2022-02-17 08:37:03 -06:00
parent ceb7df0c51
commit c5ac528a24
24 changed files with 1102 additions and 508 deletions

View File

@ -16,13 +16,11 @@ import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityMappingType; 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.NavigablePath;
import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.sql.FromClauseIndex; import org.hibernate.query.sqm.sql.FromClauseIndex;
import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslatorFactory; 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.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelect;
import org.hibernate.sql.results.graph.DomainResult; 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.internal.RowTransformerDatabaseSnapshotImpl;
import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.sql.results.spi.ListResultsConsumer;
import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.StandardBasicTypes;
@ -145,44 +142,19 @@ class DatabaseSnapshotExecutor {
} }
); );
entityDescriptor.visitStateArrayContributors(
contributorMapping -> { entityDescriptor.visitStateArrayContributors( (contributorMapping) -> {
final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() ); final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() );
if ( contributorMapping instanceof SingularAttributeMapping ) { domainResults.add(
if ( contributorMapping instanceof EntityAssociationMapping ) { contributorMapping.createSnapshotDomainResult(
domainResults.add( navigablePath,
( (EntityAssociationMapping) contributorMapping ).createDelayedDomainResult( rootTableGroup,
navigablePath, null,
rootTableGroup, state
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()
)
);
}
}
);
final SelectStatement selectStatement = new SelectStatement( rootQuerySpec, domainResults ); final SelectStatement selectStatement = new SelectStatement( rootQuerySpec, domainResults );
final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); final JdbcServices jdbcServices = sessionFactory.getJdbcServices();

View File

@ -7,6 +7,7 @@
package org.hibernate.metamodel.mapping; package org.hibernate.metamodel.mapping;
import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.sql.results.graph.DatabaseSnapshotContributor;
import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.tuple.ValueGeneration; import org.hibernate.tuple.ValueGeneration;
import org.hibernate.type.descriptor.java.MutabilityPlan; import org.hibernate.type.descriptor.java.MutabilityPlan;
@ -17,7 +18,8 @@ import org.hibernate.type.descriptor.java.MutabilityPlanExposer;
* *
* @author Steve Ebersole * @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 * The name of the mapped attribute
*/ */

View File

@ -6,11 +6,7 @@
*/ */
package org.hibernate.metamodel.mapping; 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.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 * 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(){ default boolean incrementFetchDepth(){
return true; return true;
} }
/**
* Create a delayed DomainResult for a specific reference to this ModelPart.
*/
<T> DomainResult<T> createDelayedDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState);
} }

View File

@ -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.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
import org.hibernate.sql.ast.tree.predicate.Predicate; 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.Fetchable;
import org.hibernate.sql.results.graph.FetchableContainer; import org.hibernate.sql.results.graph.FetchableContainer;
import org.hibernate.sql.results.graph.basic.BasicResult;
/** /**
* Mapping of a plural (collection-valued) attribute * Mapping of a plural (collection-valued) attribute
@ -68,6 +71,16 @@ public interface PluralAttributeMapping
fetchableConsumer.accept( getElementDescriptor() ); fetchableConsumer.accept( getElementDescriptor() );
} }
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
default <T> DomainResult<T> createSnapshotDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
return new BasicResult( 0, null, getJavaType() );
}
String getSeparateCollectionTable(); String getSeparateCollectionTable();
boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart); boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart);

View File

@ -73,6 +73,8 @@ import org.hibernate.type.EntityType;
import org.hibernate.type.Type; import org.hibernate.type.Type;
import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaType;
import static java.util.Objects.requireNonNullElse;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
*/ */
@ -380,15 +382,6 @@ public class EntityCollectionPart
: fkTargetModelPart; : fkTargetModelPart;
} }
@Override
public <T> DomainResult<T> createDelayedDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
throw new NotYetImplementedFor6Exception( getClass() );
}
@Override @Override
public JavaType<?> getJavaType() { public JavaType<?> getJavaType() {
return getEntityMappingType().getJavaType(); return getEntityMappingType().getJavaType();
@ -583,13 +576,8 @@ public class EntityCollectionPart
SqlExpressionResolver sqlExpressionResolver, SqlExpressionResolver sqlExpressionResolver,
FromClauseAccess fromClauseAccess, FromClauseAccess fromClauseAccess,
SqlAstCreationContext creationContext) { SqlAstCreationContext creationContext) {
final SqlAstJoinType joinType; final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER );
if ( requestedJoinType == null ) {
joinType = SqlAstJoinType.INNER;
}
else {
joinType = requestedJoinType;
}
if ( collectionDescriptor.isOneToMany() && nature == Nature.ELEMENT ) { 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" // If this is a one-to-many, the element part is already available, so we return a TableGroupJoin "hull"
return new TableGroupJoin( return new TableGroupJoin(
@ -645,15 +633,10 @@ public class EntityCollectionPart
SqlExpressionResolver sqlExpressionResolver, SqlExpressionResolver sqlExpressionResolver,
FromClauseAccess fromClauseAccess, FromClauseAccess fromClauseAccess,
SqlAstCreationContext creationContext) { SqlAstCreationContext creationContext) {
final SqlAstJoinType joinType; final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER );
if ( requestedJoinType == null ) {
joinType = SqlAstJoinType.INNER;
}
else {
joinType = requestedJoinType;
}
final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() ); final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() );
final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins();
final LazyTableGroup lazyTableGroup = new LazyTableGroup( final LazyTableGroup lazyTableGroup = new LazyTableGroup(
canUseInnerJoin, canUseInnerJoin,
navigablePath, navigablePath,
@ -670,7 +653,7 @@ public class EntityCollectionPart
(np, tableExpression) -> { (np, tableExpression) -> {
NavigablePath path = np.getParent(); NavigablePath path = np.getParent();
// Fast path // Fast path
if ( path != null && navigablePath.equals( path ) ) { if ( navigablePath.equals( path ) ) {
return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() )
&& fkDescriptor.getKeyTable().equals( tableExpression ); && fkDescriptor.getKeyTable().equals( tableExpression );
} }
@ -681,7 +664,7 @@ public class EntityCollectionPart
sb.insert( 0, path.getUnaliasedLocalName() ); sb.insert( 0, path.getUnaliasedLocalName() );
path = path.getParent(); path = path.getParent();
} }
return path != null && navigablePath.equals( path ) return navigablePath.equals( path )
&& targetKeyPropertyNames.contains( sb.toString() ) && targetKeyPropertyNames.contains( sb.toString() )
&& fkDescriptor.getKeyTable().equals( tableExpression ); && fkDescriptor.getKeyTable().equals( tableExpression );
}, },

View File

@ -13,6 +13,7 @@ import java.util.Iterator;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.annotations.NotFoundAction; 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.EntityFetchSelectImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityResultImpl; 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.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.CircularBiDirectionalFetchImpl;
import org.hibernate.sql.results.internal.domain.CircularFetchImpl; import org.hibernate.sql.results.internal.domain.CircularFetchImpl;
import org.hibernate.type.ComponentType; import org.hibernate.type.ComponentType;
@ -227,12 +229,14 @@ public class ToOneAttributeMapping
&& join.getPropertySpan() == 1 && join.getPropertySpan() == 1
&& join.getTable() == manyToOne.getTable() && join.getTable() == manyToOne.getTable()
&& equal( join.getKey(), manyToOne ) ) { && equal( join.getKey(), manyToOne ) ) {
//noinspection deprecation
bidirectionalAttributeName = join.getPropertyIterator().next().getName(); bidirectionalAttributeName = join.getPropertyIterator().next().getName();
break; break;
} }
} }
// Simple one-to-one mapped by cases // Simple one-to-one mapped by cases
if ( bidirectionalAttributeName == null ) { if ( bidirectionalAttributeName == null ) {
//noinspection deprecation
final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator(); final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator();
while ( propertyClosureIterator.hasNext() ) { while ( propertyClosureIterator.hasNext() ) {
final Property property = propertyClosureIterator.next(); final Property property = propertyClosureIterator.next();
@ -247,6 +251,7 @@ public class ToOneAttributeMapping
} }
} }
else { else {
//noinspection deprecation
final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator(); final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator();
while ( propertyClosureIterator.hasNext() ) { while ( propertyClosureIterator.hasNext() ) {
final Property property = propertyClosureIterator.next(); final Property property = propertyClosureIterator.next();
@ -351,7 +356,7 @@ public class ToOneAttributeMapping
else { else {
this.bidirectionalAttributeName = bidirectionalAttributeName; this.bidirectionalAttributeName = bidirectionalAttributeName;
} }
notFoundAction = isNullable() ? NotFoundAction.IGNORE : null; notFoundAction = null;
isKeyTableNullable = isNullable(); isKeyTableNullable = isNullable();
isOptional = ! bootValue.isConstrained(); isOptional = ! bootValue.isConstrained();
} }
@ -520,7 +525,9 @@ public class ToOneAttributeMapping
} }
private static boolean equal(Value lhsValue, Value rhsValue) { private static boolean equal(Value lhsValue, Value rhsValue) {
//noinspection deprecation
Iterator<Selectable> lhsColumns = lhsValue.getColumnIterator(); Iterator<Selectable> lhsColumns = lhsValue.getColumnIterator();
//noinspection deprecation
Iterator<Selectable> rhsColumns = rhsValue.getColumnIterator(); Iterator<Selectable> rhsColumns = rhsValue.getColumnIterator();
boolean hasNext; boolean hasNext;
do { do {
@ -614,9 +621,13 @@ public class ToOneAttributeMapping
: ForeignKeyDescriptor.Nature.TARGET; : ForeignKeyDescriptor.Nature.TARGET;
} }
// We can only use the parent table group if the FK is located there and ignoreNotFound is false // We can only use the parent table group if
// If this is not the case, the FK is not constrained or on a join/secondary table, so we need a join // * the FK is located there
this.canUseParentTableGroup = notFoundAction != NotFoundAction.IGNORE // * 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 && sideNature == ForeignKeyDescriptor.Nature.KEY
&& declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression );
} }
@ -635,10 +646,6 @@ public class ToOneAttributeMapping
return sideNature; return sideNature;
} }
public boolean canJoinForeignKey(EntityIdentifierMapping identifierMapping) {
return sideNature == ForeignKeyDescriptor.Nature.KEY && identifierMapping == getForeignKeyDescriptor().getTargetPart() && !isNullable;
}
public String getReferencedPropertyName() { public String getReferencedPropertyName() {
return referencedPropertyName; return referencedPropertyName;
} }
@ -1033,93 +1040,50 @@ public class ToOneAttributeMapping
&& parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() ); && parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() );
if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) { if ( hasNotFoundAction()
final TableGroup tableGroup; || ( fetchTiming == FetchTiming.IMMEDIATE && selected ) ) {
if ( fetchParent instanceof EntityResultJoinedSubclassImpl && final TableGroup tableGroup = determineTableGroup(
( (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,
fetchablePath, 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 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) { private boolean isSelectByUniqueKey(ForeignKeyDescriptor.Nature side) {
if ( side == ForeignKeyDescriptor.Nature.KEY ) { if ( side == ForeignKeyDescriptor.Nature.KEY ) {
// case 1.2 // case 1.2
@ -1203,15 +1205,21 @@ public class ToOneAttributeMapping
} }
@Override @Override
public <T> DomainResult<T> createDelayedDomainResult( public <T> DomainResult<T> createSnapshotDomainResult(
NavigablePath navigablePath, NavigablePath navigablePath,
TableGroup tableGroup, TableGroup tableGroup,
String resultVariable, String resultVariable,
DomainResultCreationState creationState) { DomainResultCreationState creationState) {
// We only need a join if the key is on the referring side i.e. this is an inverse to-one // We need a join if either
// and if the FK refers to a non-PK, in which case we must load the whole entity // - the association is mapped with `@NotFound`
if ( sideNature == ForeignKeyDescriptor.Nature.TARGET || referencedPropertyName != null ) { // - the key is on the referring side i.e. this is an inverse to-one
creationState.getSqlAstCreationState().getFromClauseAccess().resolveTableGroup( // 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, navigablePath,
np -> { np -> {
final TableGroupJoin tableGroupJoin = createTableGroupJoin( 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 ) { if ( referencedPropertyName == null ) {
//noinspection unchecked
return new EntityDelayedResultImpl( return new EntityDelayedResultImpl(
navigablePath.append( EntityIdentifierMapping.ROLE_LOCAL_NAME ), navigablePath.append( EntityIdentifierMapping.ROLE_LOCAL_NAME ),
this, this,
tableGroup, tableGroupToUse,
creationState creationState
); );
} }
@ -1241,7 +1265,8 @@ public class ToOneAttributeMapping
final EntityResultImpl entityResult = new EntityResultImpl( final EntityResultImpl entityResult = new EntityResultImpl(
navigablePath, navigablePath,
this, this,
tableGroup, null, tableGroupToUse,
null,
creationState creationState
); );
entityResult.afterInitialize( entityResult, creationState ); entityResult.afterInitialize( entityResult, creationState );
@ -1250,6 +1275,37 @@ public class ToOneAttributeMapping
} }
} }
private EntityFetch withRegisteredAssociationKeys(
Supplier<EntityFetch> 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 @Override
public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) { public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) {
if ( isKeyTableNullable || isNullable ) { if ( isKeyTableNullable || isNullable ) {
@ -1316,13 +1372,18 @@ public class ToOneAttributeMapping
break; break;
} }
} }
final SqlAstJoinType joinType; final SqlAstJoinType joinType;
if ( requestedJoinType == null ) { if ( requestedJoinType != null ) {
joinType = SqlAstJoinType.INNER;
}
else {
joinType = requestedJoinType; 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 // 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 // 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() ) if ( CollectionPart.Nature.ELEMENT.getName().equals( parentTableGroup.getNavigablePath().getUnaliasedLocalName() )
@ -1359,7 +1420,7 @@ public class ToOneAttributeMapping
} }
NavigablePath path = np.getParent(); NavigablePath path = np.getParent();
// Fast path // Fast path
if ( path != null && navigablePath.equals( path ) ) { if ( navigablePath.equals( path ) ) {
return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() )
&& identifyingColumnsTableExpression.equals( tableExpression ); && identifyingColumnsTableExpression.equals( tableExpression );
} }
@ -1379,6 +1440,7 @@ public class ToOneAttributeMapping
); );
} }
} }
final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( final LazyTableGroup lazyTableGroup = createRootTableGroupJoin(
navigablePath, navigablePath,
lhs, lhs,
@ -1400,16 +1462,23 @@ public class ToOneAttributeMapping
final TableReference lhsTableReference = lhs.resolveTableReference( navigablePath, identifyingColumnsTableExpression ); final TableReference lhsTableReference = lhs.resolveTableReference( navigablePath, identifyingColumnsTableExpression );
lazyTableGroup.setTableGroupInitializerCallback( lazyTableGroup.setTableGroupInitializerCallback( (tableGroup) -> join.applyPredicate(
tableGroup -> join.applyPredicate( foreignKeyDescriptor.generateJoinPredicate(
foreignKeyDescriptor.generateJoinPredicate( sideNature == ForeignKeyDescriptor.Nature.TARGET ? lhsTableReference : tableGroup.getPrimaryTableReference(),
sideNature == ForeignKeyDescriptor.Nature.TARGET ? lhsTableReference : tableGroup.getPrimaryTableReference(), sideNature == ForeignKeyDescriptor.Nature.TARGET ? tableGroup.getPrimaryTableReference() : lhsTableReference,
sideNature == ForeignKeyDescriptor.Nature.TARGET ? tableGroup.getPrimaryTableReference() : lhsTableReference, sqlExpressionResolver,
sqlExpressionResolver, creationContext
creationContext
)
) )
); ) );
if ( hasNotFoundAction() ) {
getAssociatedEntityMappingType().applyWhereRestrictions(
join::applyPredicate,
lazyTableGroup.getTableGroup(),
true,
null
);
}
return join; return join;
} }
@ -1427,14 +1496,17 @@ public class ToOneAttributeMapping
FromClauseAccess fromClauseAccess, FromClauseAccess fromClauseAccess,
SqlAstCreationContext creationContext) { SqlAstCreationContext creationContext) {
final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( sqlAliasStem ); final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( sqlAliasStem );
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) { final boolean canUseInnerJoin;
joinType = SqlAstJoinType.INNER; if ( ! lhs.canUseInnerJoins() ) {
canUseInnerJoin = false;
}
else if ( isNullable || hasNotFoundAction() ) {
canUseInnerJoin = false;
} }
else { else {
joinType = requestedJoinType; canUseInnerJoin = requestedJoinType == SqlAstJoinType.INNER;
} }
final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins() && !isNullable;
TableGroup realParentTableGroup = lhs; TableGroup realParentTableGroup = lhs;
while ( realParentTableGroup.getModelPart() instanceof EmbeddableValuedModelPart ) { while ( realParentTableGroup.getModelPart() instanceof EmbeddableValuedModelPart ) {
@ -1469,7 +1541,7 @@ public class ToOneAttributeMapping
} }
NavigablePath path = np.getParent(); NavigablePath path = np.getParent();
// Fast path // Fast path
if ( path != null && navigablePath.equals( path ) ) { if ( navigablePath.equals( path ) ) {
return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() ) return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() )
&& identifyingColumnsTableExpression.equals( tableExpression ); && identifyingColumnsTableExpression.equals( tableExpression );
} }
@ -1597,6 +1669,10 @@ public class ToOneAttributeMapping
return notFoundAction == NotFoundAction.IGNORE; return notFoundAction == NotFoundAction.IGNORE;
} }
public boolean hasNotFoundAction() {
return notFoundAction != null;
}
@Override @Override
public boolean isUnwrapProxy() { public boolean isUnwrapProxy() {
return unwrapProxy; return unwrapProxy;

View File

@ -16,6 +16,8 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import org.hibernate.CacheMode; import org.hibernate.CacheMode;
import org.hibernate.FlushMode; import org.hibernate.FlushMode;
@ -28,10 +30,10 @@ import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.ResultListTransformer; import org.hibernate.query.ResultListTransformer;
import org.hibernate.query.TupleTransformer; import org.hibernate.query.TupleTransformer;
import org.hibernate.query.internal.ScrollableResultsIterator; import org.hibernate.query.internal.ScrollableResultsIterator;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.spi.ScrollableResultsImplementor; 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.BasicType;
import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaType;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
/** /**
* @author Steve Ebersole * @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() ); SqlExecLogger.INSTANCE.debugf( "Reading Query result cache data per CacheMode#isGetEnabled [%s]", cacheMode.name() );
final Set<String> querySpaces = jdbcSelect.getAffectedTableNames(); final Set<String> querySpaces = jdbcSelect.getAffectedTableNames();
if ( querySpaces == null || querySpaces.size() == 0 ) { 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 { else {
SqlExecLogger.INSTANCE.tracev( "querySpaces is {0}", querySpaces ); SqlExecLogger.INSTANCE.tracef( "querySpaces is `%s`", querySpaces );
} }
final QueryResultsCache queryCache = factory.getCache() final QueryResultsCache queryCache = factory.getCache()
.getQueryResultsCache( executionContext.getQueryOptions().getResultCacheRegionName() ); .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( queryResultsCacheKey = QueryKey.from(
jdbcSelect.getSql(), jdbcSelect.getSql(),
executionContext.getQueryOptions().getLimit(), executionContext.getQueryOptions().getLimit(),
@ -558,6 +551,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, factory ); jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, factory );
metadataForCache = capturingMetadata.resolveMetadataForCache(); metadataForCache = capturingMetadata.resolveMetadataForCache();
} }
return new JdbcValuesResultSetImpl( return new JdbcValuesResultSetImpl(
resultSetAccess, resultSetAccess,
queryResultsCacheKey, queryResultsCacheKey,

View File

@ -22,8 +22,6 @@ import org.hibernate.sql.results.spi.RowTransformer;
*/ */
@Incubating @Incubating
public interface JdbcSelectExecutor { public interface JdbcSelectExecutor {
// todo (6.0) : Ideally we'd have a singular place (JdbcServices? ServiceRegistry?) to obtain these executors
<R> List<R> list( <R> List<R> list(
JdbcSelect jdbcSelect, JdbcSelect jdbcSelect,
JdbcParameterBindings jdbcParameterBindings, JdbcParameterBindings jdbcParameterBindings,

View File

@ -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.
* <p/>
* By default, simply use {@link #createDomainResult}
*/
default <T> DomainResult<T> createSnapshotDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
return createDomainResult( navigablePath, tableGroup, null, creationState );
}
}

View File

@ -22,16 +22,19 @@ import org.hibernate.sql.results.graph.entity.EntityInitializer;
*/ */
public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch { public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch {
private final DomainResult keyResult; private final DomainResult<?> keyResult;
private final boolean selectByUniqueKey; private final boolean selectByUniqueKey;
public EntityDelayedFetchImpl( public EntityDelayedFetchImpl(
FetchParent fetchParent, FetchParent fetchParent,
ToOneAttributeMapping fetchedAttribute, ToOneAttributeMapping fetchedAttribute,
NavigablePath navigablePath, NavigablePath navigablePath,
DomainResult keyResult, DomainResult<?> keyResult,
boolean selectByUniqueKey) { boolean selectByUniqueKey) {
super( navigablePath, fetchedAttribute, fetchParent ); super( navigablePath, fetchedAttribute, fetchParent );
assert fetchedAttribute.getNotFoundAction() == null;
this.keyResult = keyResult; this.keyResult = keyResult;
this.selectByUniqueKey = selectByUniqueKey; this.selectByUniqueKey = selectByUniqueKey;
} }
@ -47,7 +50,7 @@ public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch {
} }
@Override @Override
public DomainResultAssembler createAssembler( public DomainResultAssembler<?> createAssembler(
FetchParentAccess parentAccess, FetchParentAccess parentAccess,
AssemblerCreationState creationState) { AssemblerCreationState creationState) {
final NavigablePath navigablePath = getNavigablePath(); final NavigablePath navigablePath = getNavigablePath();

View File

@ -40,7 +40,7 @@ public class EntityDelayedFetchInitializer extends AbstractFetchParentAccess imp
private final NavigablePath navigablePath; private final NavigablePath navigablePath;
private final ToOneAttributeMapping referencedModelPart; private final ToOneAttributeMapping referencedModelPart;
private final boolean selectByUniqueKey; private final boolean selectByUniqueKey;
private final DomainResultAssembler identifierAssembler; private final DomainResultAssembler<?> identifierAssembler;
private Object entityInstance; private Object entityInstance;
private Object identifier; private Object identifier;
@ -50,7 +50,10 @@ public class EntityDelayedFetchInitializer extends AbstractFetchParentAccess imp
NavigablePath fetchedNavigable, NavigablePath fetchedNavigable,
ToOneAttributeMapping referencedModelPart, ToOneAttributeMapping referencedModelPart,
boolean selectByUniqueKey, boolean selectByUniqueKey,
DomainResultAssembler identifierAssembler) { DomainResultAssembler<?> identifierAssembler) {
// associations marked with `@NotFound` are ALWAYS eagerly fetched
assert referencedModelPart.getNotFoundAction() == null;
this.parentAccess = parentAccess; this.parentAccess = parentAccess;
this.navigablePath = fetchedNavigable; this.navigablePath = fetchedNavigable;
this.referencedModelPart = referencedModelPart; this.referencedModelPart = referencedModelPart;

View File

@ -24,17 +24,20 @@ import org.hibernate.sql.results.graph.entity.EntityInitializer;
* @author Andrea Boriero * @author Andrea Boriero
*/ */
public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch { public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch {
private final DomainResult keyResult; private final DomainResult<?> keyResult;
private final boolean selectByUniqueKey; private final boolean selectByUniqueKey;
public EntityFetchSelectImpl( public EntityFetchSelectImpl(
FetchParent fetchParent, FetchParent fetchParent,
ToOneAttributeMapping fetchedAttribute, ToOneAttributeMapping fetchedAttribute,
NavigablePath navigablePath, NavigablePath navigablePath,
DomainResult keyResult, DomainResult<?> keyResult,
boolean selectByUniqueKey, boolean selectByUniqueKey,
DomainResultCreationState creationState) { @SuppressWarnings("unused") DomainResultCreationState creationState) {
super( navigablePath, fetchedAttribute, fetchParent ); super( navigablePath, fetchedAttribute, fetchParent );
assert fetchedAttribute.getNotFoundAction() == null;
this.keyResult = keyResult; this.keyResult = keyResult;
this.selectByUniqueKey = selectByUniqueKey; this.selectByUniqueKey = selectByUniqueKey;
} }
@ -50,7 +53,7 @@ public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch {
} }
@Override @Override
public DomainResultAssembler createAssembler(FetchParentAccess parentAccess, AssemblerCreationState creationState) { public DomainResultAssembler<?> createAssembler(FetchParentAccess parentAccess, AssemblerCreationState creationState) {
final EntityInitializer initializer = (EntityInitializer) creationState.resolveInitializer( final EntityInitializer initializer = (EntityInitializer) creationState.resolveInitializer(
getNavigablePath(), getNavigablePath(),
getFetchedMapping(), getFetchedMapping(),

View File

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

View File

@ -0,0 +1,78 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.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<Object> createResultAssembler(
FetchParentAccess parentAccess,
AssemblerCreationState creationState) {
return new NotFoundSnapshotAssembler(
navigablePath,
toOneMapping,
keyResult.createResultAssembler( parentAccess, creationState ),
targetResult.createResultAssembler( parentAccess, creationState )
);
}
}

View File

@ -6,88 +6,93 @@
*/ */
package org.hibernate.orm.test.annotations.notfound; 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.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable; import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
/** /**
* @author Andrea Boriero * @author Andrea Boriero
*/ */
@TestForIssue(jiraKey = "HHH-11591") @JiraKey( "HHH-11591" )
@DomainModel( @DomainModel(
annotatedClasses = { OneToOneNotFoundTest.Show.class, OneToOneNotFoundTest.ShowDescription.class } annotatedClasses = { OneToOneNotFoundTest.Show.class, OneToOneNotFoundTest.ShowDescription.class }
) )
@SessionFactory( @SessionFactory
exportSchema = false
)
public class OneToOneNotFoundTest { public class OneToOneNotFoundTest {
@BeforeEach @BeforeEach
public void setUp(SessionFactoryScope scope) { public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction( (session) -> {
session -> // Show#1 will end up with a dangling foreign-key as the
session.doWork( connection -> { // matching row on the Description table is deleted
connection.createStatement().execute( {
"create table SHOW_DESCRIPTION ( ID integer not null, primary key (ID) )" ); Show show = new Show( 1, new ShowDescription( 10 ) );
connection.createStatement().execute( session.persist( show );
"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) )" );
} ) }
);
scope.inTransaction( session -> { // Show#2 will end up with a dangling foreign-key as the
Show show = new Show(); // matching row on the join-table is deleted
show.setId( 1 ); {
ShowDescription showDescription = new ShowDescription(); Show show = new Show( 2, new ShowDescription( 20 ) );
showDescription.setId( 2 ); session.persist( show );
show.setDescription( showDescription ); }
session.save( showDescription );
session.save( 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( scope.inTransaction( (session) -> session.doWork( (connection) -> {
session -> connection.createStatement().execute( "delete from descriptions where id = 10" );
session.doWork( connection -> connection.createStatement().execute( "delete from show_descriptions where description_fk = 10" );
connection.createStatement() connection.createStatement().execute( "delete from shows where id = 3" );
.execute( "delete from SHOW_DESCRIPTION where ID = 2" ) connection.createStatement().execute( "delete from show_descriptions where show_fk = 4" );
) } ) );
);
} }
@AfterEach @AfterEach
public void tearDow(SessionFactoryScope scope) { public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction( (session) -> {
session -> session.createMutationQuery( "delete ShowDescription" ).executeUpdate();
session.doWork( connection -> { session.createMutationQuery( "delete Show" ).executeUpdate();
connection.createStatement().execute( "drop table TSHOW_SHOWDESCRIPTION" ); } );
connection.createStatement().execute( "drop table SHOW_DESCRIPTION" );
connection.createStatement().execute( "drop table T_SHOW" );
} )
);
} }
@Test @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") @Entity(name = "Show")
@Table(name = "T_SHOW") @Table(name = "shows")
public static class Show { public static class Show {
@Id @Id
private Integer id; private Integer id;
@OneToOne @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
@NotFound(action = NotFoundAction.IGNORE) @NotFound(action = NotFoundAction.IGNORE)
@JoinTable(name = "TSHOW_SHOWDESCRIPTION", @JoinTable(name = "show_descriptions",
joinColumns = @JoinColumn(name = "SHOW_ID"), joinColumns = @JoinColumn(name = "show_fk"),
inverseJoinColumns = @JoinColumn(name = "DESCRIPTION_ID"), foreignKey = @ForeignKey(name = "FK_DESC")) inverseJoinColumns = @JoinColumn(name = "description_fk")
)
private ShowDescription description; 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() { public Integer getId() {
return id; return id;
@ -133,17 +175,29 @@ public class OneToOneNotFoundTest {
} }
@Entity(name = "ShowDescription") @Entity(name = "ShowDescription")
@Table(name = "SHOW_DESCRIPTION") @Table(name = "descriptions")
public static class ShowDescription { public static class ShowDescription {
@Id @Id
@Column(name = "ID") @Column(name = "id")
private Integer id; private Integer id;
@NotFound(action = NotFoundAction.IGNORE) @NotFound(action = NotFoundAction.IGNORE)
@OneToOne(mappedBy = "description", cascade = CascadeType.ALL) @OneToOne(mappedBy = "description", cascade = CascadeType.ALL)
private Show show; 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() { public Integer getId() {
return id; return id;
} }

View File

@ -33,8 +33,8 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
@ -115,33 +115,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts; final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 4 SQL statements executed // there should be 1 SQL statement with a join executed
assertEquals( 4, paramterCounts.size() ); assertThat( paramterCounts ).hasSize( 1 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
// 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() );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) { for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) {
@ -182,54 +158,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts; final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 8 SQL statements executed // there should be 1 SQL statement with a join executed
assertEquals( 8, paramterCounts.size() ); assertThat( paramterCounts ).hasSize( 1 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
// 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
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
@ -288,11 +219,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
.getEntityDescriptor( Task.class ); .getEntityDescriptor( Task.class );
final BatchFetchQueue batchFetchQueue = final BatchFetchQueue batchFetchQueue =
sessionImplementor.getPersistenceContextInternal().getBatchFetchQueue(); sessionImplementor.getPersistenceContextInternal().getBatchFetchQueue();
assertThat( assertThat( batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ) )
"Checking BatchFetchQueue for entry for Task#" + id, .describedAs( "Checking BatchFetchQueue for entry for Task#" + id )
batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ), .isEqualTo( expected );
is( expected )
);
} }
@Entity(name = "Employee") @Entity(name = "Employee")

View File

@ -85,10 +85,10 @@ public class LazyNotFoundOneToOneTest extends BaseCoreFunctionalTestCase {
this::sessionFactory, session -> { this::sessionFactory, session -> {
User user = session.find( User.class, ID ); 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() ). assertThat( sqlInterceptor.getQueryCount() ).
describedAs( "Expecting 2 queries due to `@NotFound`" ) describedAs( "Expecting 1 query (w/ join) due to `@NotFound`" )
.isEqualTo( 2 ); .isEqualTo( 1 );
assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) ) assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) )
.describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" ) .describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" )
.isTrue(); .isTrue();

View File

@ -26,8 +26,6 @@ import jakarta.persistence.ManyToOne;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
import org.hibernate.boot.SessionFactoryBuilder; import org.hibernate.boot.SessionFactoryBuilder;
@ -44,6 +42,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@ -58,7 +57,7 @@ import static org.junit.Assert.assertTrue;
@EnhancementOptions(lazyLoading = true) @EnhancementOptions(lazyLoading = true)
public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFunctionalTestCase { public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFunctionalTestCase {
private static int NUMBER_OF_ENTITIES = 20; private static final int NUMBER_OF_ENTITIES = 20;
@Test @Test
@TestForIssue(jiraKey = "HHH-11147") @TestForIssue(jiraKey = "HHH-11147")
@ -66,24 +65,20 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
final StatisticsImplementor statistics = sessionFactory().getStatistics(); final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear(); statistics.clear();
doInHibernate( inTransaction( (session) -> {
this::sessionFactory, session -> { List<Employee> employees = new ArrayList<>( NUMBER_OF_ENTITIES );
List<Employee> employees = new ArrayList<>( NUMBER_OF_ENTITIES ); for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { employees.add( session.load( Employee.class, i + 1 ) );
employees.add( session.load( Employee.class, i + 1 ) ); }
} for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { Hibernate.initialize( employees.get( i ) );
Hibernate.initialize( employees.get( i ) ); assertNull( employees.get( i ).employer );
assertNull( employees.get( i ).employer ); }
} } );
}
);
// A "not found" association cannot be batch fetched because // not-found associations are always join-fetched, so we should
// Employee#employer must be initialized immediately. // get `NUMBER_OF_ENTITIES` queries
// Enhanced proxies (and HibernateProxy objects) should never be created assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
// for a "not found" association.
assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
} }
@Test @Test
@ -92,20 +87,16 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
final StatisticsImplementor statistics = sessionFactory().getStatistics(); final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear(); statistics.clear();
doInHibernate( inTransaction( (session) -> {
this::sessionFactory, session -> { for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) { Employee employee = session.get( Employee.class, i + 1 );
Employee employee = session.get( Employee.class, i + 1 ); assertNull( employee.employer );
assertNull( employee.employer ); }
} } );
}
);
// A "not found" association cannot be batch fetched because // not-found associations are always join-fetched, so we should
// Employee#employer must be initialized immediately. // get `NUMBER_OF_ENTITIES` queries
// Enhanced proxies (and HibernateProxy objects) should never be created assertThat( statistics.getPrepareStatementCount() ).isEqualTo( NUMBER_OF_ENTITIES );
// for a "not found" association.
assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
} }
@Test @Test
@ -114,28 +105,24 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
final StatisticsImplementor statistics = sessionFactory().getStatistics(); final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear(); statistics.clear();
doInHibernate( inTransaction( (session) -> {
this::sessionFactory, session -> { for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) {
for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { Employee employee = session.get( Employee.class, i + 1 );
Employee employee = session.get( Employee.class, i + 1 ); Employer employer = new Employer();
Employer employer = new Employer(); employer.id = 2 * employee.id;
employer.id = 2 * employee.id; employer.name = "Employer #" + employer.id;
employer.name = "Employer #" + employer.id; employee.employer = employer;
employee.employer = employer; }
} } );
}
);
doInHibernate( inTransaction( (session) -> {
this::sessionFactory, session -> { for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) {
for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) { Employee employee = session.get( Employee.class, i + 1 );
Employee employee = session.get( Employee.class, i + 1 ); assertTrue( Hibernate.isInitialized( employee.employer ) );
assertTrue( Hibernate.isInitialized( employee.employer ) ); assertEquals( employee.id * 2, employee.employer.id );
assertEquals( employee.id * 2, employee.employer.id ); assertEquals( "Employer #" + employee.employer.id, employee.employer.name );
assertEquals( "Employer #" + employee.employer.id, employee.employer.name ); }
} } );
}
);
} }
@Override @Override

View File

@ -70,10 +70,9 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
} }
); );
// The Employee#employer must be initialized immediately because // not-found associations are always join-fetched, so we should
// enhanced proxies (and HibernateProxy objects) should never be created // get 1 query for the Employee with join
// for a "not found" association. assertEquals( 1, statistics.getPrepareStatementCount() );
assertEquals( 2, statistics.getPrepareStatementCount() );
} }
@Test @Test
@ -89,10 +88,9 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
} }
); );
// The Employee#employer must be initialized immediately because // not-found associations are always join-fetched, so we should
// enhanced proxies (and HibernateProxy objects) should never be created // get 1 query for the Employee with join
// for a "not found" association. assertEquals( 1, statistics.getPrepareStatementCount() );
assertEquals( 2, statistics.getPrepareStatementCount() );
} }
@Test @Test

View File

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

View File

@ -50,8 +50,8 @@ import static org.junit.jupiter.api.Assertions.fail;
public class NotFoundExceptionLogicalOneToOneTest { public class NotFoundExceptionLogicalOneToOneTest {
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) { public void testProxyCurrency(SessionFactoryScope scope) {
// test handling of a proxy for the missing Coin // test handling of a proxy for the missing Currency
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final Currency proxy = session.byId( Currency.class ).getReference( 1 ); final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try { 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 @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
public void testGet(SessionFactoryScope scope) { public void testGet(SessionFactoryScope scope) {
@ -74,22 +91,18 @@ public class NotFoundExceptionLogicalOneToOneTest {
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
session.get( Coin.class, 2 ); session.get( Coin.class, 2 );
// at the moment this is handled as SELECT fetch assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
} ); } );
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
try { try {
final Coin coin = session.get( Coin.class, 1 ); 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) { catch (FetchNotFoundException expected) {
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() ); 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<Coin> 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<Coin> 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<Coin> 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<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
} );
}
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
@FailureExpected( reason = "Join is not used in the SQL" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) { public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
@ -111,8 +210,11 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( coins ).isEmpty(); assertThat( coins ).isEmpty();
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} ); } );
statementInspector.clear(); statementInspector.clear();
@ -123,6 +225,8 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( coins ).hasSize( 1 ); assertThat( coins ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); 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<Currency> 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 @BeforeEach
public void prepareTestData(SessionFactoryScope scope) { public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {

View File

@ -50,9 +50,8 @@ public class NotFoundExceptionManyToOneTest {
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) { public void testProxyCurrency(SessionFactoryScope scope) {
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
// the non-existent Child
final Currency proxy = session.byId( Currency.class ).getReference( 1 ); final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try { try {
Hibernate.initialize( proxy ); 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 @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
public void testGet(SessionFactoryScope scope) { public void testGet(SessionFactoryScope scope) {
@ -78,8 +93,9 @@ public class NotFoundExceptionManyToOneTest {
fail( "Expecting ObjectNotFoundException - " + coin.getCurrency() ); fail( "Expecting ObjectNotFoundException - " + coin.getCurrency() );
} }
catch (FetchNotFoundException expected) { catch (FetchNotFoundException expected) {
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); 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<Coin> 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<Coin> 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<Coin> 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<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
} );
}
@Test @Test
@JiraKey( "HHH-15060" ) @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) { public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
try { final String hql = "select c from Coin c where c.currency.id = 1";
final String hql = "select c from Coin c where c.currency.id = 1"; final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
session.createQuery( hql, Coin.class ).getResultList(); assertThat( coins ).isEmpty();
fail( "Expecting ObjectNotFoundException for broken fk" ); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
} assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
catch (ObjectNotFoundException expected) { assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( expected.getIdentifier() ).isEqualTo( 1 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
}
} ); } );
} }
@ -140,13 +233,20 @@ public class NotFoundExceptionManyToOneTest {
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
public void testQueryAssociationSelection(SessionFactoryScope scope) { public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// NOTE: this one is not obvious // NOTE: this one is not obvious
// - we are selecting the association so from that perspective, throwing the ObjectNotFoundException is nice // - 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 // - the other way to look at it is that there are simply no matching results, so nothing to return
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c where c.id = 1"; final String hql = "select c.currency from Coin c where c.id = 1";
final List<Currency> resultList = session.createQuery( hql, Currency.class ).getResultList(); final List<Currency> 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 " );
} ); } );
} }

View File

@ -112,39 +112,32 @@ public class NotFoundIgnoreManyToOneTest {
assertThat( coins ).hasSize( 1 ); assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull(); 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( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
} ); } );
} }
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
@FailureExpected( // @FailureExpected( reason = "Has zero results because of bad join" )
reason = "Has zero results because of inner-join; & the select w/ inner-join is executed twice for some odd reason"
)
public void testQueryAssociationSelection(SessionFactoryScope scope) { public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final String hql = "select c.id, c.currency from Coin c"; final String hql = "select c.id, c.currency from Coin c";
final List<Tuple> tuples = session.createSelectionQuery( hql, Tuple.class ).getResultList(); final List<Tuple> tuples = session.createQuery( hql, Tuple.class ).getResultList();
assertThat( tuples ).hasSize( 1 ); assertThat( tuples ).hasSize( 0 );
final Tuple tuple = tuples.get( 0 );
assertThat( tuple.get( 0 ) ).isEqualTo( 1 );
assertThat( tuple.get( 1 ) ).isNull();
assertThat( statementInspector.getSqlQueries() ).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 ) ).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(); 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 // I guess this one is somewhat debatable, but for consistency I think this makes the most sense
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c"; final String hql = "select c.currency from Coin c";
session.createQuery( hql, Currency.class ).getResultList();
final List<Currency> currencies = session.createSelectionQuery( hql, Currency.class ).getResultList(); final List<Currency> currencies = session.createSelectionQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 1 ); assertThat( currencies ).hasSize( 0 );
assertThat( currencies.get( 0 ) ).isNull();
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} ); } );
} }

View File

@ -21,7 +21,6 @@ import org.hibernate.annotations.NotFoundAction;
import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel; 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.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScope;
@ -75,16 +74,17 @@ public class NotFoundIgnoreOneToOneTest {
final Coin coin = session.get( Coin.class, 1 ); final Coin coin = session.get( Coin.class, 1 );
assertThat( coin.getCurrency() ).isNull(); assertThat( coin.getCurrency() ).isNull();
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} ); } );
} }
@Test @Test
@JiraKey( "HHH-15060" ) @JiraKey( "HHH-15060" )
@FailureExpected( reason = "Bad results due to join" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) { public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
@ -92,11 +92,11 @@ public class NotFoundIgnoreOneToOneTest {
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.id = 1"; final String hql = "select c from Coin c where c.currency.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList(); final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 ); assertThat( coins ).isEmpty();
assertThat( coins.get( 0 ).getCurrency() ).isNull();
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).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 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} ); } );
@ -114,37 +114,42 @@ public class NotFoundIgnoreOneToOneTest {
assertThat( coins ).hasSize( 1 ); assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull(); 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( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
} ); } );
} }
@Test @Test
@JiraKey( "HHH-15060" ) @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) { public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> { scope.inTransaction( (session) -> {
final String hql = "select c.id, c.currency from Coin c"; final String hql = "select c.id, c.currency from Coin c";
final List<Tuple> tuples = session.createQuery( hql, Tuple.class ).getResultList(); final List<Tuple> tuples = session.createQuery( hql, Tuple.class ).getResultList();
assertThat( tuples ).hasSize( 1 ); assertThat( tuples ).hasSize( 0 );
final Tuple tuple = tuples.get( 0 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( tuple.get( 0 ) ).isEqualTo( 1 ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( tuple.get( 1 ) ).isNull(); 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) -> { scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c"; final String hql = "select c.currency from Coin c";
final List<Currency> currencies = session.createQuery( hql, Currency.class ).getResultList(); final List<Currency> currencies = session.createQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 1 ); assertThat( currencies ).hasSize( 0 );
assertThat( currencies.get( 0 ) ).isNull(); 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 " );
} ); } );
} }