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.SharedSessionContractImplementor;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.SingularAttributeMapping;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.spi.NavigablePath;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.sql.FromClauseIndex;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -44,7 +42,6 @@ import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcSelect;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.basic.BasicResult;
import org.hibernate.sql.results.internal.RowTransformerDatabaseSnapshotImpl;
import org.hibernate.sql.results.spi.ListResultsConsumer;
import org.hibernate.type.StandardBasicTypes;
@ -145,44 +142,19 @@ class DatabaseSnapshotExecutor {
}
);
entityDescriptor.visitStateArrayContributors(
contributorMapping -> {
final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() );
if ( contributorMapping instanceof SingularAttributeMapping ) {
if ( contributorMapping instanceof EntityAssociationMapping ) {
domainResults.add(
( (EntityAssociationMapping) contributorMapping ).createDelayedDomainResult(
navigablePath,
rootTableGroup,
null,
state
)
);
}
else {
domainResults.add(
contributorMapping.createDomainResult(
navigablePath,
rootTableGroup,
null,
state
)
);
}
}
else {
// TODO: Instead use a delayed collection result? Or will we remove this when redesigning this
//noinspection unchecked
domainResults.add(
new BasicResult(
0,
null,
contributorMapping.getJavaType()
)
);
}
}
);
entityDescriptor.visitStateArrayContributors( (contributorMapping) -> {
final NavigablePath navigablePath = rootPath.append( contributorMapping.getAttributeName() );
domainResults.add(
contributorMapping.createSnapshotDomainResult(
navigablePath,
rootTableGroup,
null,
state
)
);
} );
final SelectStatement selectStatement = new SelectStatement( rootQuerySpec, domainResults );
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();

View File

@ -7,6 +7,7 @@
package org.hibernate.metamodel.mapping;
import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.sql.results.graph.DatabaseSnapshotContributor;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.tuple.ValueGeneration;
import org.hibernate.type.descriptor.java.MutabilityPlan;
@ -17,7 +18,8 @@ import org.hibernate.type.descriptor.java.MutabilityPlanExposer;
*
* @author Steve Ebersole
*/
public interface AttributeMapping extends ModelPart, ValueMapping, Fetchable, PropertyBasedMapping, MutabilityPlanExposer {
public interface AttributeMapping
extends ModelPart, ValueMapping, Fetchable, DatabaseSnapshotContributor, PropertyBasedMapping, MutabilityPlanExposer {
/**
* The name of the mapped attribute
*/

View File

@ -6,11 +6,7 @@
*/
package org.hibernate.metamodel.mapping;
import org.hibernate.query.spi.NavigablePath;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
/**
* Commonality between `many-to-one`, `one-to-one` and `any`, as well as entity-valued collection elements and map-keys
@ -35,13 +31,4 @@ public interface EntityAssociationMapping extends ModelPart, Association, TableG
default boolean incrementFetchDepth(){
return true;
}
/**
* Create a delayed DomainResult for a specific reference to this ModelPart.
*/
<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.TableGroupJoinProducer;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.FetchableContainer;
import org.hibernate.sql.results.graph.basic.BasicResult;
/**
* Mapping of a plural (collection-valued) attribute
@ -68,6 +71,16 @@ public interface PluralAttributeMapping
fetchableConsumer.accept( getElementDescriptor() );
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
default <T> DomainResult<T> createSnapshotDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
return new BasicResult( 0, null, getJavaType() );
}
String getSeparateCollectionTable();
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.descriptor.java.JavaType;
import static java.util.Objects.requireNonNullElse;
/**
* @author Steve Ebersole
*/
@ -380,15 +382,6 @@ public class EntityCollectionPart
: fkTargetModelPart;
}
@Override
public <T> DomainResult<T> createDelayedDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
throw new NotYetImplementedFor6Exception( getClass() );
}
@Override
public JavaType<?> getJavaType() {
return getEntityMappingType().getJavaType();
@ -583,13 +576,8 @@ public class EntityCollectionPart
SqlExpressionResolver sqlExpressionResolver,
FromClauseAccess fromClauseAccess,
SqlAstCreationContext creationContext) {
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) {
joinType = SqlAstJoinType.INNER;
}
else {
joinType = requestedJoinType;
}
final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER );
if ( collectionDescriptor.isOneToMany() && nature == Nature.ELEMENT ) {
// If this is a one-to-many, the element part is already available, so we return a TableGroupJoin "hull"
return new TableGroupJoin(
@ -645,15 +633,10 @@ public class EntityCollectionPart
SqlExpressionResolver sqlExpressionResolver,
FromClauseAccess fromClauseAccess,
SqlAstCreationContext creationContext) {
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) {
joinType = SqlAstJoinType.INNER;
}
else {
joinType = requestedJoinType;
}
final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER );
final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() );
final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins();
final LazyTableGroup lazyTableGroup = new LazyTableGroup(
canUseInnerJoin,
navigablePath,
@ -670,7 +653,7 @@ public class EntityCollectionPart
(np, tableExpression) -> {
NavigablePath path = np.getParent();
// Fast path
if ( path != null && navigablePath.equals( path ) ) {
if ( navigablePath.equals( path ) ) {
return targetKeyPropertyNames.contains( np.getUnaliasedLocalName() )
&& fkDescriptor.getKeyTable().equals( tableExpression );
}
@ -681,7 +664,7 @@ public class EntityCollectionPart
sb.insert( 0, path.getUnaliasedLocalName() );
path = path.getParent();
}
return path != null && navigablePath.equals( path )
return navigablePath.equals( path )
&& targetKeyPropertyNames.contains( sb.toString() )
&& fkDescriptor.getKeyTable().equals( tableExpression );
},

View File

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

View File

@ -16,6 +16,8 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
@ -28,10 +30,10 @@ import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.ResultListTransformer;
import org.hibernate.query.TupleTransformer;
import org.hibernate.query.internal.ScrollableResultsIterator;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.spi.ScrollableResultsImplementor;
@ -65,9 +67,6 @@ import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.BasicType;
import org.hibernate.type.descriptor.java.JavaType;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
/**
* @author Steve Ebersole
*/
@ -478,21 +477,15 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
SqlExecLogger.INSTANCE.debugf( "Reading Query result cache data per CacheMode#isGetEnabled [%s]", cacheMode.name() );
final Set<String> querySpaces = jdbcSelect.getAffectedTableNames();
if ( querySpaces == null || querySpaces.size() == 0 ) {
SqlExecLogger.INSTANCE.tracev( "Unexpected querySpaces is {0}", ( querySpaces == null ? querySpaces : "empty" ) );
SqlExecLogger.INSTANCE.tracef( "Unexpected querySpaces is empty" );
}
else {
SqlExecLogger.INSTANCE.tracev( "querySpaces is {0}", querySpaces );
SqlExecLogger.INSTANCE.tracef( "querySpaces is `%s`", querySpaces );
}
final QueryResultsCache queryCache = factory.getCache()
.getQueryResultsCache( executionContext.getQueryOptions().getResultCacheRegionName() );
// todo (6.0) : not sure that it is at all important that we account for QueryResults
// these cached values are "lower level" than that, representing the
// "raw" JDBC values.
//
// todo (6.0) : relatedly ^^, pretty sure that SqlSelections are also irrelevant
queryResultsCacheKey = QueryKey.from(
jdbcSelect.getSql(),
executionContext.getQueryOptions().getLimit(),
@ -558,6 +551,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, factory );
metadataForCache = capturingMetadata.resolveMetadataForCache();
}
return new JdbcValuesResultSetImpl(
resultSetAccess,
queryResultsCacheKey,

View File

@ -22,8 +22,6 @@ import org.hibernate.sql.results.spi.RowTransformer;
*/
@Incubating
public interface JdbcSelectExecutor {
// todo (6.0) : Ideally we'd have a singular place (JdbcServices? ServiceRegistry?) to obtain these executors
<R> List<R> list(
JdbcSelect jdbcSelect,
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 {
private final DomainResult keyResult;
private final DomainResult<?> keyResult;
private final boolean selectByUniqueKey;
public EntityDelayedFetchImpl(
FetchParent fetchParent,
ToOneAttributeMapping fetchedAttribute,
NavigablePath navigablePath,
DomainResult keyResult,
DomainResult<?> keyResult,
boolean selectByUniqueKey) {
super( navigablePath, fetchedAttribute, fetchParent );
assert fetchedAttribute.getNotFoundAction() == null;
this.keyResult = keyResult;
this.selectByUniqueKey = selectByUniqueKey;
}
@ -47,7 +50,7 @@ public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch {
}
@Override
public DomainResultAssembler createAssembler(
public DomainResultAssembler<?> createAssembler(
FetchParentAccess parentAccess,
AssemblerCreationState creationState) {
final NavigablePath navigablePath = getNavigablePath();

View File

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

View File

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

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;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeMetamodelsImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.FailureExpected;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* @author Andrea Boriero
*/
@TestForIssue(jiraKey = "HHH-11591")
@JiraKey( "HHH-11591" )
@DomainModel(
annotatedClasses = { OneToOneNotFoundTest.Show.class, OneToOneNotFoundTest.ShowDescription.class }
)
@SessionFactory(
exportSchema = false
)
@SessionFactory
public class OneToOneNotFoundTest {
@BeforeEach
public void setUp(SessionFactoryScope scope) {
scope.inTransaction(
session ->
session.doWork( connection -> {
connection.createStatement().execute(
"create table SHOW_DESCRIPTION ( ID integer not null, primary key (ID) )" );
connection.createStatement().execute(
"create table T_SHOW ( id integer not null, primary key (id) )" );
connection.createStatement().execute(
"create table TSHOW_SHOWDESCRIPTION ( DESCRIPTION_ID integer, SHOW_ID integer not null, primary key (SHOW_ID) )" );
public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// Show#1 will end up with a dangling foreign-key as the
// matching row on the Description table is deleted
{
Show show = new Show( 1, new ShowDescription( 10 ) );
session.persist( show );
} )
);
}
scope.inTransaction( session -> {
Show show = new Show();
show.setId( 1 );
ShowDescription showDescription = new ShowDescription();
showDescription.setId( 2 );
show.setDescription( showDescription );
session.save( showDescription );
session.save( show );
// Show#2 will end up with a dangling foreign-key as the
// matching row on the join-table is deleted
{
Show show = new Show( 2, new ShowDescription( 20 ) );
session.persist( show );
}
// Show#3 will end up as an inverse dangling foreign-key from
// Description because the matching row is deleted from the
// Show table
{
Show show = new Show( 3, new ShowDescription( 30 ) );
session.persist( show );
}
// Show#4 will end up as an inverse dangling foreign-key from
// Description because the matching row is deleted from the
// join-table
{
Show show = new Show( 4, new ShowDescription( 40 ) );
session.persist( show );
}
} );
scope.inTransaction(
session ->
session.doWork( connection ->
connection.createStatement()
.execute( "delete from SHOW_DESCRIPTION where ID = 2" )
)
);
scope.inTransaction( (session) -> session.doWork( (connection) -> {
connection.createStatement().execute( "delete from descriptions where id = 10" );
connection.createStatement().execute( "delete from show_descriptions where description_fk = 10" );
connection.createStatement().execute( "delete from shows where id = 3" );
connection.createStatement().execute( "delete from show_descriptions where show_fk = 4" );
} ) );
}
@AfterEach
public void tearDow(SessionFactoryScope scope) {
scope.inTransaction(
session ->
session.doWork( connection -> {
connection.createStatement().execute( "drop table TSHOW_SHOWDESCRIPTION" );
connection.createStatement().execute( "drop table SHOW_DESCRIPTION" );
connection.createStatement().execute( "drop table T_SHOW" );
} )
);
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete ShowDescription" ).executeUpdate();
session.createMutationQuery( "delete Show" ).executeUpdate();
} );
}
@Test
@ -99,20 +104,57 @@ public class OneToOneNotFoundTest {
} );
}
@Test
public void databaseSnapshotTest(SessionFactoryScope scope) throws Exception {
final SessionFactoryImplementor sessionFactory = scope.getSessionFactory();
final RuntimeMetamodelsImplementor runtimeMetamodels = sessionFactory.getRuntimeMetamodels();
// Check the Show side
scope.inTransaction( (session) -> {
final EntityMappingType showMapping = runtimeMetamodels.getEntityMappingType( Show.class );
final Object[] databaseSnapshot = showMapping.getEntityPersister().getDatabaseSnapshot( 1, session );
// `Show#description` is the only state-array-contributor for Show
assertThat( databaseSnapshot ).describedAs( "`Show` database-snapshot" ).hasSize( 1 );
// the snapshot value for `Show#description` should be null
assertThat( databaseSnapshot[0] ).describedAs( "`Show#description` database-snapshot value" ).isNull();
} );
// Check the ShowDescription side
scope.inTransaction( (session) -> {
final EntityMappingType descriptionMapping = runtimeMetamodels.getEntityMappingType( ShowDescription.class );
final Object[] databaseSnapshot = descriptionMapping.getEntityPersister().getDatabaseSnapshot( 2, session );
assertThat( databaseSnapshot ).describedAs( "`ShowDescription` database snapshot" ).isNull();
} );
}
@Entity(name = "Show")
@Table(name = "T_SHOW")
@Table(name = "shows")
public static class Show {
@Id
private Integer id;
@OneToOne
@OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
@NotFound(action = NotFoundAction.IGNORE)
@JoinTable(name = "TSHOW_SHOWDESCRIPTION",
joinColumns = @JoinColumn(name = "SHOW_ID"),
inverseJoinColumns = @JoinColumn(name = "DESCRIPTION_ID"), foreignKey = @ForeignKey(name = "FK_DESC"))
@JoinTable(name = "show_descriptions",
joinColumns = @JoinColumn(name = "show_fk"),
inverseJoinColumns = @JoinColumn(name = "description_fk")
)
private ShowDescription description;
protected Show() {
}
public Show(Integer id, ShowDescription description) {
this.id = id;
this.description = description;
if ( description != null ) {
description.setShow( this );
}
}
public Integer getId() {
return id;
@ -133,17 +175,29 @@ public class OneToOneNotFoundTest {
}
@Entity(name = "ShowDescription")
@Table(name = "SHOW_DESCRIPTION")
@Table(name = "descriptions")
public static class ShowDescription {
@Id
@Column(name = "ID")
@Column(name = "id")
private Integer id;
@NotFound(action = NotFoundAction.IGNORE)
@OneToOne(mappedBy = "description", cascade = CascadeType.ALL)
private Show show;
protected ShowDescription() {
}
public ShowDescription(Integer id) {
this.id = id;
}
public ShowDescription(Integer id, Show show) {
this.id = id;
this.show = show;
}
public Integer getId() {
return id;
}

View File

@ -33,8 +33,8 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -115,33 +115,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 4 SQL statements executed
assertEquals( 4, paramterCounts.size() );
// query loading Employee entities shouldn't have any parameters
assertEquals( 0, paramterCounts.get( 0 ).intValue() );
// query specifically for Task with ID == 0 will result in 1st batch;
// query should have 5 parameters for [0,1,2,3,4];
// Task with ID == 1 won't be found; the rest will be found.
assertEquals( 5, paramterCounts.get( 1 ).intValue() );
// query specifically for Task with ID == 1 will result in 2nd batch;
// query should have 4 parameters [1,5,6,7];
// Task with IDs == [1,7] won't be found; the rest will be found.
assertEquals( 4, paramterCounts.get( 2 ).intValue() );
// no extra queries required to load entities with IDs [2,3,4] because they
// were already loaded from 1st batch
// no extra queries required to load entities with IDs [5,6] because they
// were already loaded from 2nd batch
// query specifically for Task with ID == 7 will result in just querying
// Task with ID == 7 (because the batch is empty).
// query should have 1 parameter [7];
// Task with ID == 7 won't be found.
assertEquals( 1, paramterCounts.get( 3 ).intValue() );
// there should be 1 SQL statement with a join executed
assertThat( paramterCounts ).hasSize( 1 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) {
@ -182,54 +158,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 8 SQL statements executed
assertEquals( 8, paramterCounts.size() );
// query loading Employee entities shouldn't have any parameters
assertEquals( 0, paramterCounts.get( 0 ).intValue() );
// query specifically for Task with ID == 0 will result in 1st batch;
// query should have 5 parameters for [0,1,2,3,4];
// Task with IDs == [0,1,2,3,4] won't be found
assertEquals( 5, paramterCounts.get( 1 ).intValue() );
// query specifically for Task with ID == 1 will result in 2nd batch;
// query should have 4 parameters [1,5,6,7];
// Task with IDs == [1,5,6] won't be found; Task with ID == 7 will be found.
assertEquals( 4, paramterCounts.get( 2 ).intValue() );
// query specifically for Task with ID == 2 will result in just querying
// Task with ID == 2 (because the batch is empty).
// query should have 1 parameter [2];
// Task with ID == 2 won't be found.
assertEquals( 1, paramterCounts.get( 3 ).intValue() );
// query specifically for Task with ID == 3 will result in just querying
// Task with ID == 3 (because the batch is empty).
// query should have 1 parameter [3];
// Task with ID == 3 won't be found.
assertEquals( 1, paramterCounts.get( 4 ).intValue() );
// query specifically for Task with ID == 4 will result in just querying
// Task with ID == 4 (because the batch is empty).
// query should have 1 parameter [4];
// Task with ID == 4 won't be found.
assertEquals( 1, paramterCounts.get( 5 ).intValue() );
// query specifically for Task with ID == 5 will result in just querying
// Task with ID == 5 (because the batch is empty).
// query should have 1 parameter [5];
// Task with ID == 5 won't be found.
assertEquals( 1, paramterCounts.get( 6 ).intValue() );
// query specifically for Task with ID == 6 will result in just querying
// Task with ID == 6 (because the batch is empty).
// query should have 1 parameter [6];
// Task with ID == 6 won't be found.
assertEquals( 1, paramterCounts.get( 7 ).intValue() );
// no extra queries required to load entity with ID == 7 because it
// was already loaded from 2nd batch
// there should be 1 SQL statement with a join executed
assertThat( paramterCounts ).hasSize( 1 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
@ -288,11 +219,9 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
.getEntityDescriptor( Task.class );
final BatchFetchQueue batchFetchQueue =
sessionImplementor.getPersistenceContextInternal().getBatchFetchQueue();
assertThat(
"Checking BatchFetchQueue for entry for Task#" + id,
batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ),
is( expected )
);
assertThat( batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ) )
.describedAs( "Checking BatchFetchQueue for entry for Task#" + id )
.isEqualTo( expected );
}
@Entity(name = "Employee")

View File

@ -85,10 +85,10 @@ public class LazyNotFoundOneToOneTest extends BaseCoreFunctionalTestCase {
this::sessionFactory, session -> {
User user = session.find( User.class, ID );
// per UserGuide (and simply correct behavior), `@NotFound` forces EAGER fetching
// `@NotFound` forces EAGER join fetching
assertThat( sqlInterceptor.getQueryCount() ).
describedAs( "Expecting 2 queries due to `@NotFound`" )
.isEqualTo( 2 );
describedAs( "Expecting 1 query (w/ join) due to `@NotFound`" )
.isEqualTo( 1 );
assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) )
.describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" )
.isTrue();

View File

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

View File

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

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 {
@Test
@JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) {
// test handling of a proxy for the missing Coin
public void testProxyCurrency(SessionFactoryScope scope) {
// test handling of a proxy for the missing Currency
scope.inTransaction( (session) -> {
final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try {
@ -65,6 +65,23 @@ public class NotFoundExceptionLogicalOneToOneTest {
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testProxyCoin(SessionFactoryScope scope) {
// test handling of a proxy for the missing Coin
scope.inTransaction( (session) -> {
final Coin proxy = session.byId( Coin.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
Assertions.fail( "Expecting ObjectNotFoundException" );
}
catch (FetchNotFoundException expected) {
assertThat( expected.getEntityName() ).endsWith( "Currency" );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testGet(SessionFactoryScope scope) {
@ -74,22 +91,18 @@ public class NotFoundExceptionLogicalOneToOneTest {
scope.inTransaction( (session) -> {
session.get( Coin.class, 2 );
// at the moment this is handled as SELECT fetch
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
scope.inTransaction( (session) -> {
try {
final Coin coin = session.get( Coin.class, 1 );
fail( "Expecting ObjectNotFoundException, got - coin = " + coin + "; currency = " + coin.currency );
fail( "Expecting FetchNotFoundException, got - coin = " + coin + "; currency = " + coin.currency );
}
catch (FetchNotFoundException expected) {
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
@ -98,9 +111,95 @@ public class NotFoundExceptionLogicalOneToOneTest {
} );
}
/**
* Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want
* SQL generated there to behave exactly the same as this query - specifically forcing the
* join
*/
@Test
@JiraKey( "HHH-15060" )
public void testQueryImplicitPathDereferencePredicateBaseline(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.name = 'Euro'";
final List<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
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "Join is not used in the SQL" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -111,8 +210,11 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( coins ).isEmpty();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
statementInspector.clear();
@ -123,6 +225,8 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( coins ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
@ -152,6 +256,23 @@ public class NotFoundExceptionLogicalOneToOneTest {
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c where c.id = 1";
final List<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
public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {

View File

@ -50,9 +50,8 @@ public class NotFoundExceptionManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) {
public void testProxyCurrency(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// the non-existent Child
final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
@ -65,6 +64,22 @@ public class NotFoundExceptionManyToOneTest {
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testProxyCoin(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Coin proxy = session.byId( Coin.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
Assertions.fail( "Expecting ObjectNotFoundException" );
}
catch (FetchNotFoundException expected) {
assertThat( expected.getEntityName() ).endsWith( "Currency" );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testGet(SessionFactoryScope scope) {
@ -78,8 +93,9 @@ public class NotFoundExceptionManyToOneTest {
fail( "Expecting ObjectNotFoundException - " + coin.getCurrency() );
}
catch (FetchNotFoundException expected) {
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
@ -89,32 +105,109 @@ public class NotFoundExceptionManyToOneTest {
} );
}
/**
* Baseline for {@link #testQueryImplicitPathDereferencePredicate}. Ultimately, we want
* SQL generated there to behave exactly the same as this query - specifically forcing the
* join
*/
@Test
@JiraKey( "HHH-15060" )
public void testQueryImplicitPathDereferencePredicateBaseline(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.name = 'Euro'";
final List<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
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "Does not do the join. Instead selects the Coin based on `currency_id` and then " +
"subsequent-selects the Currency. Ultimately results in a `Coin#1` reference with a " +
"null Currency."
)
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
try {
final String hql = "select c from Coin c where c.currency.id = 1";
session.createQuery( hql, Coin.class ).getResultList();
final String hql = "select c from Coin c where c.currency.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).isEmpty();
fail( "Expecting ObjectNotFoundException for broken fk" );
}
catch (ObjectNotFoundException expected) {
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
}
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
}
@ -140,13 +233,20 @@ public class NotFoundExceptionManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// NOTE: this one is not obvious
// - we are selecting the association so from that perspective, throwing the ObjectNotFoundException is nice
// - the other way to look at it is that there are simply no matching results, so nothing to return
scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c where c.id = 1";
final List<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.get( 0 ).getCurrency() ).isNull();
// at the moment this uses a subsequent-select. on the bright side, it is at least eagerly fetched.
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
}
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "Has zero results because of inner-join; & the select w/ inner-join is executed twice for some odd reason"
)
// @FailureExpected( reason = "Has zero results because of bad join" )
public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.id, c.currency from Coin c";
final List<Tuple> tuples = session.createSelectionQuery( hql, Tuple.class ).getResultList();
assertThat( tuples ).hasSize( 1 );
final Tuple tuple = tuples.get( 0 );
assertThat( tuple.get( 0 ) ).isEqualTo( 1 );
assertThat( tuple.get( 1 ) ).isNull();
final List<Tuple> tuples = session.createQuery( hql, Tuple.class ).getResultList();
assertThat( tuples ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
statementInspector.clear();
@ -152,14 +145,15 @@ public class NotFoundIgnoreManyToOneTest {
// I guess this one is somewhat debatable, but for consistency I think this makes the most sense
scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c";
session.createQuery( hql, Currency.class ).getResultList();
final List<Currency> currencies = session.createSelectionQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 1 );
assertThat( currencies.get( 0 ) ).isNull();
assertThat( currencies ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}

View File

@ -21,7 +21,6 @@ import org.hibernate.annotations.NotFoundAction;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.FailureExpected;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
@ -75,16 +74,17 @@ public class NotFoundIgnoreOneToOneTest {
final Coin coin = session.get( Coin.class, 1 );
assertThat( coin.getCurrency() ).isNull();
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
}
@Test
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "Bad results due to join" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -92,11 +92,11 @@ public class NotFoundIgnoreOneToOneTest {
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( coins ).isEmpty();
// technically we could use a subsequent-select rather than a join...
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
@ -114,37 +114,42 @@ public class NotFoundIgnoreOneToOneTest {
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
// at the moment this uses a subsequent-select. on the bright side, it is at least eagerly fetched.
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " );
} );
}
@Test
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "Has zero results because of join; & the select w/ join is executed twice for some yet-unknown reason" )
public void testQueryAssociationSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.id, c.currency from Coin c";
final List<Tuple> tuples = session.createQuery( hql, Tuple.class ).getResultList();
assertThat( tuples ).hasSize( 1 );
final Tuple tuple = tuples.get( 0 );
assertThat( tuple.get( 0 ) ).isEqualTo( 1 );
assertThat( tuple.get( 1 ) ).isNull();
assertThat( tuples ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c";
final List<Currency> currencies = session.createQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 1 );
assertThat( currencies.get( 0 ) ).isNull();
assertThat( currencies ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}