HHH-15099 - Improve handling of associations marked with @NotFound
- database snapshot handling
This commit is contained in:
parent
ceb7df0c51
commit
c5ac528a24
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 );
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 )
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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) -> {
|
||||
|
|
|
@ -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 " );
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -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 " );
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -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 " );
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue