HHH-17384 Fix `@NotFound` to-one association nullness handling
This commit is contained in:
parent
4d3df8d81f
commit
3cfd85d8e2
|
@ -263,6 +263,11 @@ public class DerbyLegacySqlAstTranslator<T extends JdbcOperation> extends Abstra
|
||||||
return getDialect().getVersion().isSameOrAfter( 10, 5 );
|
return getDialect().getVersion().isSameOrAfter( 10, 5 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsJoinInMutationStatementSubquery() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) {
|
public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) {
|
||||||
final BinaryArithmeticOperator operator = arithmeticExpression.getOperator();
|
final BinaryArithmeticOperator operator = arithmeticExpression.getOperator();
|
||||||
|
|
|
@ -302,4 +302,9 @@ public class H2LegacySqlAstTranslator<T extends JdbcOperation> extends AbstractS
|
||||||
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
||||||
return getDialect().getVersion().isSameOrAfter( 1, 4, 198 );
|
return getDialect().getVersion().isSameOrAfter( 1, 4, 198 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsJoinInMutationStatementSubquery() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,6 +261,11 @@ public class DerbySqlAstTranslator<T extends JdbcOperation> extends AbstractSqlA
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsJoinInMutationStatementSubquery() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) {
|
public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) {
|
||||||
final BinaryArithmeticOperator operator = arithmeticExpression.getOperator();
|
final BinaryArithmeticOperator operator = arithmeticExpression.getOperator();
|
||||||
|
|
|
@ -281,4 +281,9 @@ public class H2SqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslato
|
||||||
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsJoinInMutationStatementSubquery() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1534,7 +1534,7 @@ public class ToOneAttributeMapping
|
||||||
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
|
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
|
||||||
// If the key side is non-nullable we also need to add the keyResult
|
// If the key side is non-nullable we also need to add the keyResult
|
||||||
// to be able to manually check invalid foreign key references
|
// to be able to manually check invalid foreign key references
|
||||||
if ( notFoundAction != null || !isInternalLoadNullable ) {
|
if ( hasNotFoundAction() || !isInternalLoadNullable ) {
|
||||||
keyResult = foreignKeyDescriptor.createKeyDomainResult(
|
keyResult = foreignKeyDescriptor.createKeyDomainResult(
|
||||||
fetchablePath,
|
fetchablePath,
|
||||||
tableGroup,
|
tableGroup,
|
||||||
|
@ -1543,7 +1543,7 @@ public class ToOneAttributeMapping
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ( notFoundAction != null
|
else if ( hasNotFoundAction()
|
||||||
|| getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) {
|
|| getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) {
|
||||||
// For the target side only add keyResult when a not-found action is present
|
// For the target side only add keyResult when a not-found action is present
|
||||||
keyResult = foreignKeyDescriptor.createTargetDomainResult(
|
keyResult = foreignKeyDescriptor.createTargetDomainResult(
|
||||||
|
|
|
@ -3575,10 +3575,14 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
}
|
}
|
||||||
|
|
||||||
private <X> X prepareReusablePath(SqmPath<?> sqmPath, Supplier<X> supplier) {
|
private <X> X prepareReusablePath(SqmPath<?> sqmPath, Supplier<X> supplier) {
|
||||||
return prepareReusablePath( sqmPath, fromClauseIndexStack.getCurrent(), supplier );
|
return prepareReusablePath( sqmPath, fromClauseIndexStack.getCurrent(), supplier, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
private <X> X prepareReusablePath(SqmPath<?> sqmPath, FromClauseIndex fromClauseIndex, Supplier<X> supplier) {
|
private <X> X prepareReusablePath(
|
||||||
|
SqmPath<?> sqmPath,
|
||||||
|
FromClauseIndex fromClauseIndex,
|
||||||
|
Supplier<X> supplier,
|
||||||
|
boolean allowLeftJoins) {
|
||||||
final Consumer<TableGroup> implicitJoinChecker;
|
final Consumer<TableGroup> implicitJoinChecker;
|
||||||
if ( getCurrentClauseStack().getCurrent() != Clause.SET_EXPRESSION ) {
|
if ( getCurrentClauseStack().getCurrent() != Clause.SET_EXPRESSION ) {
|
||||||
implicitJoinChecker = tg -> {};
|
implicitJoinChecker = tg -> {};
|
||||||
|
@ -3601,7 +3605,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
fromClauseIndex.getTableGroup( sqmPath.getLhs().getNavigablePath() ),
|
fromClauseIndex.getTableGroup( sqmPath.getLhs().getNavigablePath() ),
|
||||||
sqmPath
|
sqmPath
|
||||||
),
|
),
|
||||||
sqmPath
|
sqmPath,
|
||||||
|
allowLeftJoins
|
||||||
);
|
);
|
||||||
if ( createdTableGroup != null ) {
|
if ( createdTableGroup != null ) {
|
||||||
if ( sqmPath instanceof SqmTreatedPath<?, ?> ) {
|
if ( sqmPath instanceof SqmTreatedPath<?, ?> ) {
|
||||||
|
@ -3655,7 +3660,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
newTableGroup = getActualTableGroup(
|
newTableGroup = getActualTableGroup(
|
||||||
createTableGroup( createdParentTableGroup, parentPath ),
|
createTableGroup( createdParentTableGroup, parentPath, false ),
|
||||||
sqmPath
|
sqmPath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3669,9 +3674,21 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
fromClauseIndex.register( sqmPath, parentTableGroup );
|
fromClauseIndex.register( sqmPath, parentTableGroup );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( parentPath instanceof SqmSimplePath<?>
|
upgradeToInnerJoinIfNeeded( parentTableGroup, sqmPath, parentPath, fromClauseIndex );
|
||||||
|
|
||||||
|
registerPathAttributeEntityNameUsage( sqmPath, parentTableGroup );
|
||||||
|
|
||||||
|
return parentTableGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upgradeToInnerJoinIfNeeded(
|
||||||
|
TableGroup parentTableGroup,
|
||||||
|
SqmPath<?> sqmPath,
|
||||||
|
SqmPath<?> parentPath,
|
||||||
|
FromClauseIndex fromClauseIndex) {
|
||||||
|
if ( getCurrentClauseStack().getCurrent() != Clause.SELECT
|
||||||
|
&& parentPath instanceof SqmSimplePath<?>
|
||||||
&& CollectionPart.Nature.fromName( parentPath.getNavigablePath().getLocalName() ) == null
|
&& CollectionPart.Nature.fromName( parentPath.getNavigablePath().getLocalName() ) == null
|
||||||
&& getCurrentClauseStack().getCurrent() != Clause.SELECT
|
|
||||||
&& parentPath.getParentPath() != null
|
&& parentPath.getParentPath() != null
|
||||||
&& parentTableGroup.getModelPart() instanceof ToOneAttributeMapping ) {
|
&& parentTableGroup.getModelPart() instanceof ToOneAttributeMapping ) {
|
||||||
// we need to handle the case of an implicit path involving a to-one
|
// we need to handle the case of an implicit path involving a to-one
|
||||||
|
@ -3695,9 +3712,6 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerPathAttributeEntityNameUsage( sqmPath, parentTableGroup );
|
|
||||||
|
|
||||||
return parentTableGroup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareForSelection(SqmPath<?> selectionPath) {
|
private void prepareForSelection(SqmPath<?> selectionPath) {
|
||||||
|
@ -3723,7 +3737,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
// But only create it for paths that are not handled by #prepareReusablePath anyway
|
// But only create it for paths that are not handled by #prepareReusablePath anyway
|
||||||
final TableGroup createdTableGroup = createTableGroup(
|
final TableGroup createdTableGroup = createTableGroup(
|
||||||
getActualTableGroup( fromClauseIndex.getTableGroup( path.getLhs().getNavigablePath() ), path ),
|
getActualTableGroup( fromClauseIndex.getTableGroup( path.getLhs().getNavigablePath() ), path ),
|
||||||
path
|
path,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
if ( createdTableGroup != null ) {
|
if ( createdTableGroup != null ) {
|
||||||
registerEntityNameProjectionUsage( path, createdTableGroup );
|
registerEntityNameProjectionUsage( path, createdTableGroup );
|
||||||
|
@ -3744,7 +3759,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TableGroup createTableGroup(TableGroup parentTableGroup, SqmPath<?> joinedPath) {
|
private TableGroup createTableGroup(TableGroup parentTableGroup, SqmPath<?> joinedPath, boolean allowLeftJoins) {
|
||||||
final SqmPath<?> lhsPath = joinedPath.getLhs();
|
final SqmPath<?> lhsPath = joinedPath.getLhs();
|
||||||
final FromClauseIndex fromClauseIndex = getFromClauseIndex();
|
final FromClauseIndex fromClauseIndex = getFromClauseIndex();
|
||||||
final ModelPart subPart = parentTableGroup.getModelPart().findSubPart(
|
final ModelPart subPart = parentTableGroup.getModelPart().findSubPart(
|
||||||
|
@ -3776,18 +3791,30 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
querySpec.getFromClause().addRoot( tableGroup );
|
querySpec.getFromClause().addRoot( tableGroup );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Check if we can reuse a table group join of the parent
|
final TableGroupJoin compatibleLeftJoin;
|
||||||
final TableGroup compatibleTableGroup = parentTableGroup.findCompatibleJoinedGroup(
|
final SqlAstJoinType sqlAstJoinType;
|
||||||
|
if ( isMappedByOrNotFoundToOne( joinProducer ) ) {
|
||||||
|
compatibleLeftJoin = parentTableGroup.findCompatibleJoin(
|
||||||
joinProducer,
|
joinProducer,
|
||||||
SqlAstJoinType.INNER
|
SqlAstJoinType.LEFT
|
||||||
);
|
);
|
||||||
|
sqlAstJoinType = SqlAstJoinType.LEFT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
compatibleLeftJoin = null;
|
||||||
|
sqlAstJoinType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TableGroup compatibleTableGroup = compatibleLeftJoin != null ?
|
||||||
|
compatibleLeftJoin.getJoinedGroup() :
|
||||||
|
parentTableGroup.findCompatibleJoinedGroup( joinProducer, SqlAstJoinType.INNER );
|
||||||
if ( compatibleTableGroup == null ) {
|
if ( compatibleTableGroup == null ) {
|
||||||
final TableGroupJoin tableGroupJoin = joinProducer.createTableGroupJoin(
|
final TableGroupJoin tableGroupJoin = joinProducer.createTableGroupJoin(
|
||||||
joinedPath.getNavigablePath(),
|
joinedPath.getNavigablePath(),
|
||||||
parentTableGroup,
|
parentTableGroup,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
allowLeftJoins ? sqlAstJoinType : null,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
this
|
this
|
||||||
|
@ -3807,6 +3834,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
// Also register the table group under its original navigable path, which possibly contains an alias
|
// Also register the table group under its original navigable path, which possibly contains an alias
|
||||||
// This is important, as otherwise we might create new joins in subqueries which are unnecessary
|
// This is important, as otherwise we might create new joins in subqueries which are unnecessary
|
||||||
fromClauseIndex.registerTableGroup( tableGroup.getNavigablePath(), tableGroup );
|
fromClauseIndex.registerTableGroup( tableGroup.getNavigablePath(), tableGroup );
|
||||||
|
// Upgrade the join type to inner if the context doesn't allow left joins
|
||||||
|
if ( compatibleLeftJoin != null && !allowLeftJoins ) {
|
||||||
|
compatibleLeftJoin.setJoinType( SqlAstJoinType.INNER );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3819,6 +3850,16 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
return tableGroup;
|
return tableGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isMappedByOrNotFoundToOne(TableGroupJoinProducer joinProducer) {
|
||||||
|
if ( joinProducer instanceof ToOneAttributeMapping ) {
|
||||||
|
final ToOneAttributeMapping toOne = (ToOneAttributeMapping) joinProducer;
|
||||||
|
if ( toOne.hasNotFoundAction() || toOne.getReferencedPropertyName() != null ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isRecursiveCte(TableGroup tableGroup) {
|
private boolean isRecursiveCte(TableGroup tableGroup) {
|
||||||
if ( tableGroup instanceof CteTableGroup ) {
|
if ( tableGroup instanceof CteTableGroup ) {
|
||||||
final CteTableGroup cteTableGroup = (CteTableGroup) tableGroup;
|
final CteTableGroup cteTableGroup = (CteTableGroup) tableGroup;
|
||||||
|
@ -7674,12 +7715,31 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NullnessPredicate visitIsNullPredicate(SqmNullnessPredicate predicate) {
|
public NullnessPredicate visitIsNullPredicate(SqmNullnessPredicate predicate) {
|
||||||
return new NullnessPredicate(
|
final SqmExpression<?> sqmExpression = predicate.getExpression();
|
||||||
(Expression) visitWithInferredType( predicate.getExpression(), () -> basicType( Object.class )),
|
final Expression expression;
|
||||||
predicate.isNegated(),
|
if ( sqmExpression instanceof SqmEntityValuedSimplePath<?> ) {
|
||||||
getBooleanType()
|
final SqmEntityValuedSimplePath<?> entityValuedPath = (SqmEntityValuedSimplePath<?>) sqmExpression;
|
||||||
|
inferrableTypeAccessStack.push( () -> basicType( Object.class ) );
|
||||||
|
expression = withTreatRestriction( prepareReusablePath(
|
||||||
|
entityValuedPath,
|
||||||
|
fromClauseIndexStack.getCurrent(),
|
||||||
|
() -> EntityValuedPathInterpretation.from(
|
||||||
|
entityValuedPath,
|
||||||
|
getInferredValueMapping(),
|
||||||
|
this
|
||||||
|
),
|
||||||
|
true
|
||||||
|
), entityValuedPath );
|
||||||
|
inferrableTypeAccessStack.pop();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expression = (Expression) visitWithInferredType(
|
||||||
|
predicate.getExpression(),
|
||||||
|
() -> basicType( Object.class )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return new NullnessPredicate( expression, predicate.isNegated(), getBooleanType() );
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object visitIsTruePredicate(SqmTruthnessPredicate predicate) {
|
public Object visitIsTruePredicate(SqmTruthnessPredicate predicate) {
|
||||||
|
|
|
@ -221,12 +221,8 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If the mapping is an inverse association, use the PK and disallow FK optimizations
|
// If the mapping is an inverse association, use the PK and disallow FK optimizations
|
||||||
resultModelPart = ( (EntityAssociationMapping) mapping ).getAssociatedEntityMappingType().getIdentifierMapping();
|
resultModelPart = associationMapping.getAssociatedEntityMappingType().getIdentifierMapping();
|
||||||
resultTableGroup = tableGroup;
|
resultTableGroup = tableGroup;
|
||||||
|
|
||||||
// todo (not-found) : in the case of not-found=ignore, we want to do the join, however -
|
|
||||||
// * use a left join when the association is the path terminus (`root.association`)
|
|
||||||
// * use an inner join when it is further de-referenced (`root.association.stuff`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ( mapping instanceof AnonymousTupleEntityValuedModelPart ) {
|
else if ( mapping instanceof AnonymousTupleEntityValuedModelPart ) {
|
||||||
|
|
|
@ -79,6 +79,7 @@ import org.hibernate.query.sqm.function.MultipatternSqmFunctionDescriptor;
|
||||||
import org.hibernate.query.sqm.function.SelfRenderingAggregateFunctionSqlAstExpression;
|
import org.hibernate.query.sqm.function.SelfRenderingAggregateFunctionSqlAstExpression;
|
||||||
import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression;
|
import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression;
|
||||||
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
|
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
|
||||||
|
import org.hibernate.query.sqm.sql.internal.EntityValuedPathInterpretation;
|
||||||
import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation;
|
import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation;
|
||||||
import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation;
|
import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation;
|
||||||
import org.hibernate.query.sqm.tree.expression.Conversion;
|
import org.hibernate.query.sqm.tree.expression.Conversion;
|
||||||
|
@ -143,6 +144,8 @@ import org.hibernate.sql.ast.tree.from.LazyTableGroup;
|
||||||
import org.hibernate.sql.ast.tree.from.NamedTableReference;
|
import org.hibernate.sql.ast.tree.from.NamedTableReference;
|
||||||
import org.hibernate.sql.ast.tree.from.QueryPartTableGroup;
|
import org.hibernate.sql.ast.tree.from.QueryPartTableGroup;
|
||||||
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
|
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
|
||||||
|
import org.hibernate.sql.ast.tree.from.StandardTableGroup;
|
||||||
|
import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup;
|
||||||
import org.hibernate.sql.ast.tree.from.TableGroup;
|
import org.hibernate.sql.ast.tree.from.TableGroup;
|
||||||
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
|
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
|
||||||
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
|
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
|
||||||
|
@ -1092,6 +1095,61 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
||||||
querySpec.getSelectClause().addSqlSelection(
|
querySpec.getSelectClause().addSqlSelection(
|
||||||
new SqlSelectionImpl( new QueryLiteral<>( 1, getIntegerType() ) )
|
new SqlSelectionImpl( new QueryLiteral<>( 1, getIntegerType() ) )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final List<TableGroupJoin> collectedNonInnerJoins;
|
||||||
|
if ( supportsJoinInMutationStatementSubquery() ) {
|
||||||
|
collectedNonInnerJoins = new ArrayList<>();
|
||||||
|
emulateWhereClauseRestrictionJoins( statement, querySpec, tableGroupJoin -> {
|
||||||
|
if ( tableGroupJoin.getJoinType() == SqlAstJoinType.INNER ) {
|
||||||
|
final TableGroup joinedGroup = tableGroupJoin.getJoinedGroup();
|
||||||
|
final FromClause fromClause = querySpec.getFromClause();
|
||||||
|
if ( fromClause.getRoots().isEmpty() ) {
|
||||||
|
final TableGroup copy = new StandardTableGroup(
|
||||||
|
joinedGroup.canUseInnerJoins(),
|
||||||
|
joinedGroup.getNavigablePath(),
|
||||||
|
(TableGroupProducer) joinedGroup.getModelPart(),
|
||||||
|
joinedGroup.getSourceAlias(),
|
||||||
|
joinedGroup.getPrimaryTableReference(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
fromClause.addRoot( copy );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fromClause.addRoot( joinedGroup );
|
||||||
|
}
|
||||||
|
querySpec.applyPredicate( tableGroupJoin.getPredicate() );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
collectedNonInnerJoins.add( tableGroupJoin );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
collectedNonInnerJoins = null;
|
||||||
|
emulateWhereClauseRestrictionJoins( statement, querySpec, tableGroupJoin -> {
|
||||||
|
if ( tableGroupJoin.getJoinType() == SqlAstJoinType.INNER ) {
|
||||||
|
querySpec.getFromClause().addRoot( tableGroupJoin.getJoinedGroup() );
|
||||||
|
querySpec.applyPredicate( tableGroupJoin.getPredicate() );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( querySpec.getFromClause().getRoots().isEmpty() ) {
|
||||||
|
return statement.getRestriction();
|
||||||
|
}
|
||||||
|
else if ( collectedNonInnerJoins != null ) {
|
||||||
|
collectedNonInnerJoins.forEach( querySpec.getFromClause().getRoots().get( 0 )::addTableGroupJoin );
|
||||||
|
}
|
||||||
|
|
||||||
|
querySpec.applyPredicate( statement.getRestriction() );
|
||||||
|
return new ExistsPredicate( querySpec, false, getBooleanType() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emulateWhereClauseRestrictionJoins(
|
||||||
|
AbstractUpdateOrDeleteStatement statement,
|
||||||
|
QuerySpec querySpec,
|
||||||
|
Consumer<TableGroupJoin> joinConsumer) {
|
||||||
for ( TableGroup root : statement.getFromClause().getRoots() ) {
|
for ( TableGroup root : statement.getFromClause().getRoots() ) {
|
||||||
if ( root.getPrimaryTableReference() == statement.getTargetTable() ) {
|
if ( root.getPrimaryTableReference() == statement.getTargetTable() ) {
|
||||||
for ( TableReferenceJoin tableReferenceJoin : root.getTableReferenceJoins() ) {
|
for ( TableReferenceJoin tableReferenceJoin : root.getTableReferenceJoins() ) {
|
||||||
|
@ -1106,23 +1164,13 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
||||||
);
|
);
|
||||||
querySpec.applyPredicate( tableReferenceJoin.getPredicate() );
|
querySpec.applyPredicate( tableReferenceJoin.getPredicate() );
|
||||||
}
|
}
|
||||||
for ( TableGroupJoin tableGroupJoin : root.getTableGroupJoins() ) {
|
root.getTableGroupJoins().forEach( joinConsumer );
|
||||||
assert tableGroupJoin.getJoinType() == SqlAstJoinType.INNER;
|
root.getNestedTableGroupJoins().forEach( joinConsumer );
|
||||||
querySpec.getFromClause().addRoot( tableGroupJoin.getJoinedGroup() );
|
|
||||||
querySpec.applyPredicate( tableGroupJoin.getPredicate() );
|
|
||||||
}
|
|
||||||
for ( TableGroupJoin tableGroupJoin : root.getNestedTableGroupJoins() ) {
|
|
||||||
assert tableGroupJoin.getJoinType() == SqlAstJoinType.INNER;
|
|
||||||
querySpec.getFromClause().addRoot( tableGroupJoin.getJoinedGroup() );
|
|
||||||
querySpec.applyPredicate( tableGroupJoin.getPredicate() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
querySpec.getFromClause().addRoot( root );
|
querySpec.getFromClause().addRoot( root );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
querySpec.applyPredicate( statement.getRestriction() );
|
|
||||||
return new ExistsPredicate( querySpec, false, getBooleanType() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void renderSetClause(UpdateStatement statement, Stack<Clause> clauseStack) {
|
protected void renderSetClause(UpdateStatement statement, Stack<Clause> clauseStack) {
|
||||||
|
@ -7460,11 +7508,66 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
||||||
separator = " and ";
|
separator = " and ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if ( expression instanceof EntityValuedPathInterpretation<?> ) {
|
||||||
|
final AbstractUpdateOrDeleteStatement statement = getCurrentOrParentUpdateOrDeleteStatement( !supportsJoinInMutationStatementSubquery() );
|
||||||
|
if ( statement != null ) {
|
||||||
|
final TableGroup tableGroup = ( (EntityValuedPathInterpretation<?>) expression ).getTableGroup();
|
||||||
|
final TableGroupJoin tableGroupJoin = findTableGroupJoin(
|
||||||
|
tableGroup,
|
||||||
|
statement.getFromClause().getRoots()
|
||||||
|
);
|
||||||
|
if ( tableGroupJoin != null && tableGroupJoin.getJoinType() != SqlAstJoinType.INNER ) {
|
||||||
|
emulateNullnessPredicateWithExistsSubquery( nullnessPredicate, tableGroup, tableGroupJoin );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
expression.accept( this );
|
expression.accept( this );
|
||||||
appendSql( predicateValue );
|
appendSql( predicateValue );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void emulateNullnessPredicateWithExistsSubquery(
|
||||||
|
NullnessPredicate nullnessPredicate,
|
||||||
|
TableGroup tableGroup,
|
||||||
|
TableGroupJoin tableGroupJoin) {
|
||||||
|
final QuerySpec querySpec = new QuerySpec( false );
|
||||||
|
querySpec.getSelectClause().addSqlSelection(
|
||||||
|
new SqlSelectionImpl( new QueryLiteral<>( 1, getIntegerType() ) )
|
||||||
|
);
|
||||||
|
querySpec.getFromClause().getRoots().add( tableGroup );
|
||||||
|
querySpec.applyPredicate( tableGroupJoin.getPredicate() );
|
||||||
|
|
||||||
|
if ( !nullnessPredicate.isNegated() ) {
|
||||||
|
appendSql( "not " );
|
||||||
|
}
|
||||||
|
appendSql( "exists(" );
|
||||||
|
statementStack.push( new SelectStatement( querySpec ) );
|
||||||
|
visitQuerySpec( querySpec );
|
||||||
|
statementStack.pop();
|
||||||
|
appendSql( ")" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractUpdateOrDeleteStatement getCurrentOrParentUpdateOrDeleteStatement(boolean checkParent) {
|
||||||
|
if ( statementStack.getCurrent() instanceof AbstractUpdateOrDeleteStatement ) {
|
||||||
|
return (AbstractUpdateOrDeleteStatement) statementStack.getCurrent();
|
||||||
|
}
|
||||||
|
else if ( checkParent && statementStack.depth() > 1
|
||||||
|
&& statementStack.peek( 1 ) instanceof AbstractUpdateOrDeleteStatement ) {
|
||||||
|
return (AbstractUpdateOrDeleteStatement) statementStack.peek( 1 );
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableGroupJoin findTableGroupJoin(TableGroup tableGroup, List<TableGroup> roots) {
|
||||||
|
for ( TableGroup root : roots ) {
|
||||||
|
final TableGroupJoin tableGroupJoin = root.findTableGroupJoin( tableGroup );
|
||||||
|
if ( tableGroupJoin != null ) {
|
||||||
|
return tableGroupJoin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -7795,6 +7898,14 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
||||||
return supportsRowValueConstructorSyntaxInInList();
|
return supportsRowValueConstructorSyntaxInInList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the dialect supports using joins in mutation statement subquery
|
||||||
|
* that could also use columns from the mutation target table
|
||||||
|
*/
|
||||||
|
protected boolean supportsJoinInMutationStatementSubquery() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some databases require a bit of syntactic noise when
|
* Some databases require a bit of syntactic noise when
|
||||||
* there are no tables in the from clause.
|
* there are no tables in the from clause.
|
||||||
|
|
|
@ -161,7 +161,7 @@ public interface TableGroup extends SqlAstNode, ColumnReferenceQualifier, SqmPat
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
default TableGroup findCompatibleJoinedGroup(
|
default TableGroupJoin findCompatibleJoin(
|
||||||
TableGroupJoinProducer joinProducer,
|
TableGroupJoinProducer joinProducer,
|
||||||
SqlAstJoinType requestedJoinType) {
|
SqlAstJoinType requestedJoinType) {
|
||||||
// We don't look into nested table group joins as that wouldn't be "compatible"
|
// We don't look into nested table group joins as that wouldn't be "compatible"
|
||||||
|
@ -178,13 +178,20 @@ public interface TableGroup extends SqlAstNode, ColumnReferenceQualifier, SqmPat
|
||||||
// regardless of the join type or predicate since the LHS is the same table group
|
// regardless of the join type or predicate since the LHS is the same table group
|
||||||
// If this is a left join though, we have to check if the predicate is simply the association predicate
|
// If this is a left join though, we have to check if the predicate is simply the association predicate
|
||||||
if ( joinType == SqlAstJoinType.INNER || joinProducer.isSimpleJoinPredicate( join.getPredicate() ) ) {
|
if ( joinType == SqlAstJoinType.INNER || joinProducer.isSimpleJoinPredicate( join.getPredicate() ) ) {
|
||||||
return join.getJoinedGroup();
|
return join;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default TableGroup findCompatibleJoinedGroup(
|
||||||
|
TableGroupJoinProducer joinProducer,
|
||||||
|
SqlAstJoinType requestedJoinType) {
|
||||||
|
final TableGroupJoin compatibleJoin = findCompatibleJoin( joinProducer, requestedJoinType );
|
||||||
|
return compatibleJoin != null ? compatibleJoin.getJoinedGroup() : null;
|
||||||
|
}
|
||||||
|
|
||||||
default TableGroupJoin findTableGroupJoin(TableGroup tableGroup) {
|
default TableGroupJoin findTableGroupJoin(TableGroup tableGroup) {
|
||||||
for ( TableGroupJoin join : getTableGroupJoins() ) {
|
for ( TableGroupJoin join : getTableGroupJoins() ) {
|
||||||
if ( join.getJoinedGroup() == tableGroup ) {
|
if ( join.getJoinedGroup() == tableGroup ) {
|
||||||
|
|
|
@ -0,0 +1,330 @@
|
||||||
|
/*
|
||||||
|
* 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.mapping.mappedBy;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
||||||
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.Jira;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Marco Belladelli
|
||||||
|
*
|
||||||
|
* @see org.hibernate.orm.test.notfound.IsNullAndNotFoundTest
|
||||||
|
*/
|
||||||
|
@DomainModel( annotatedClasses = {
|
||||||
|
IsNullAndMappedByTest.Person.class,
|
||||||
|
IsNullAndMappedByTest.Account.class,
|
||||||
|
} )
|
||||||
|
@SessionFactory( useCollectingStatementInspector = true )
|
||||||
|
@Jira( "https://hibernate.atlassian.net/browse/HHH-17384" )
|
||||||
|
public class IsNullAndMappedByTest {
|
||||||
|
@BeforeAll
|
||||||
|
public void setUp(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
final Person person1 = new Person( 1, "Luigi" );
|
||||||
|
final Person person2 = new Person( 2, "Andrea" );
|
||||||
|
final Person person3 = new Person( 3, "Max" );
|
||||||
|
|
||||||
|
final Account account1 = new Account( 1, null, null, person1 );
|
||||||
|
final Account account2 = new Account( 2, "Fab", null, person2 );
|
||||||
|
final Account account3 = new Account( 3, "And", null, null );
|
||||||
|
|
||||||
|
session.persist( person1 );
|
||||||
|
session.persist( person2 );
|
||||||
|
session.persist( person3 );
|
||||||
|
session.persist( account1 );
|
||||||
|
session.persist( account2 );
|
||||||
|
session.persist( account3 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public void tearDown(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
session.createMutationQuery( "delete from Account" ).executeUpdate();
|
||||||
|
session.createMutationQuery( "delete from Person" ).executeUpdate();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAssociationDereferenceIsNullInWhereClause(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
// should produce an inner join to ACCOUNT_TABLE
|
||||||
|
|
||||||
|
final List<Integer> ids = session.createQuery(
|
||||||
|
"select p.id from Person p where p.account.code is null",
|
||||||
|
Integer.class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, ids.size() );
|
||||||
|
assertEquals( 1, (int) ids.get( 0 ) );
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( " join " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( " left " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( " ACCOUNT_TABLE " );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAssociationIsNullInWhereClause(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
// should produce a left join to ACCOUNT_TABLE and restrict based on the Account's id -
|
||||||
|
//
|
||||||
|
// ...
|
||||||
|
// from PERSON p
|
||||||
|
// left join ACCOUNT_TABLE a
|
||||||
|
// on p.account_id = a.id
|
||||||
|
// where a.id is null
|
||||||
|
|
||||||
|
final List<Integer> ids = session.createQuery(
|
||||||
|
"select distinct p.id from Person p where p.account is null",
|
||||||
|
Integer.class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, ids.size() );
|
||||||
|
assertEquals( 3, (int) ids.get( 0 ) );
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( " left join " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( ".id is null" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFetchedAssociationIsNullInWhereClause(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
// should produce an inner join to ACCOUNT_TABLE since it's explicitly selected
|
||||||
|
//
|
||||||
|
// ...
|
||||||
|
// from PERSON p
|
||||||
|
// join ACCOUNT_TABLE a
|
||||||
|
// on p.account_id = a.id
|
||||||
|
// where a.id is null
|
||||||
|
|
||||||
|
final List<Account> results = session.createQuery(
|
||||||
|
"select p.account from Person p where p.account is null",
|
||||||
|
Account.class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
assertThat( results ).isEmpty();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "join" );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( " left join " );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsNullInWhereClause3(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
final List<Integer> ids = session.createQuery(
|
||||||
|
"select distinct a.id from Account a where fk(a.person) is null",
|
||||||
|
Integer.class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, ids.size() );
|
||||||
|
assertEquals( 3, (int) ids.get( 0 ) );
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( " join " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( ".person_id is null" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAssociationEqualsInWhereClause(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
// at the moment -
|
||||||
|
//
|
||||||
|
// select
|
||||||
|
// distinct p1_0.id
|
||||||
|
// from
|
||||||
|
// Person p1_0
|
||||||
|
// join
|
||||||
|
// ACCOUNT_TABLE a1_0
|
||||||
|
// on a1_0.id=p1_0.account_id
|
||||||
|
// where
|
||||||
|
// a1_0.id=?
|
||||||
|
|
||||||
|
final List<Integer> ids = session.createQuery(
|
||||||
|
"select distinct p.id from Person p where p.account = :acct",
|
||||||
|
Integer.class
|
||||||
|
).setParameter( "acct", new Account( 1, null, null, null ) ).getResultList();
|
||||||
|
|
||||||
|
assertThat( ids ).hasSize( 1 );
|
||||||
|
assertThat( ids.get( 0 ) ).isEqualTo( 1 );
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( " join " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( ".id=?" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsNullInWhereClause5(SessionFactoryScope scope) {
|
||||||
|
scope.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",
|
||||||
|
Integer.class
|
||||||
|
)
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, ids.size() );
|
||||||
|
assertEquals( 1, (int) ids.get( 0 ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWhereClause(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
final List<Integer> ids = session.createQuery(
|
||||||
|
"select p.id from Person p where p.account.code = :code and p.account.id = :id",
|
||||||
|
Integer.class
|
||||||
|
)
|
||||||
|
.setParameter( "code", "Fab" )
|
||||||
|
.setParameter( "id", 2 )
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, ids.size() );
|
||||||
|
assertEquals( 2, (int) ids.get( 0 ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
scope.inTransaction( (entityManager) -> {
|
||||||
|
entityManager.createMutationQuery( "delete from Person p where p.account is null" ).executeUpdate();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
// could physically be a join or exists sub-query
|
||||||
|
assertThat( inspector.getSqlQueries()
|
||||||
|
.get( 0 ) ).matches( (sql) -> sql.contains( "left join" ) || sql.contains( "not exists" ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHqlUpdate(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
scope.inTransaction( (entityManager) -> {
|
||||||
|
entityManager.createMutationQuery( "update Person p set p.name = 'abc' where p.account is null" ).executeUpdate();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
// could physically be a join or exists sub-query
|
||||||
|
assertThat( inspector.getSqlQueries()
|
||||||
|
.get( 0 ) ).matches( (sql) -> sql.contains( "left join" ) || sql.contains( "not exists" ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHqlUpdateSet(SessionFactoryScope scope) {
|
||||||
|
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
scope.inTransaction( (entityManager) -> {
|
||||||
|
entityManager.createMutationQuery( "update Account a set a.person = null where id = 99" ).executeUpdate();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( " join " );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "person_id=null" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Entity( name = "Person" )
|
||||||
|
public static class Person {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@OneToOne( mappedBy = "person" )
|
||||||
|
private Account account;
|
||||||
|
|
||||||
|
public Person() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Person(Integer id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account getAccount() {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings( { "FieldCanBeLocal", "unused" } )
|
||||||
|
@Entity( name = "Account" )
|
||||||
|
@Table( name = "ACCOUNT_TABLE" )
|
||||||
|
public static class Account {
|
||||||
|
@Id
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
private Double amount;
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
private Person person;
|
||||||
|
|
||||||
|
public Account() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account(Integer id, String code, Double amount, Person person) {
|
||||||
|
this.id = id;
|
||||||
|
this.code = code;
|
||||||
|
this.amount = amount;
|
||||||
|
this.person = person;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,16 +13,15 @@ import org.hibernate.annotations.NotFoundAction;
|
||||||
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
|
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
|
||||||
import org.hibernate.cfg.AvailableSettings;
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
|
|
||||||
import org.hibernate.testing.FailureExpected;
|
|
||||||
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
||||||
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
|
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
|
||||||
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
|
|
||||||
import org.hibernate.testing.orm.junit.Jira;
|
import org.hibernate.testing.orm.junit.Jira;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.OneToOne;
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
@ -99,10 +98,6 @@ public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@FailureExpected(
|
|
||||||
jiraKey = "HHH-17143",
|
|
||||||
message = "Conceptually this should render as a left join because of the path terminal; currently uses inner join"
|
|
||||||
)
|
|
||||||
public void testAssociationIsNullInWhereClause() {
|
public void testAssociationIsNullInWhereClause() {
|
||||||
inTransaction(
|
inTransaction(
|
||||||
session -> {
|
session -> {
|
||||||
|
@ -130,6 +125,33 @@ public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFetchedAssociationIsNullInWhereClause() {
|
||||||
|
inTransaction(
|
||||||
|
session -> {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
// should produce an inner join to ACCOUNT_TABLE since it's explicitly selected
|
||||||
|
//
|
||||||
|
// ...
|
||||||
|
// from PERSON p
|
||||||
|
// join ACCOUNT_TABLE a
|
||||||
|
// on p.account_id = a.id
|
||||||
|
// where a.id is null
|
||||||
|
|
||||||
|
final List<Account> results = session.createQuery(
|
||||||
|
"select p.account from Person p where p.account is null", Account.class )
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
assertThat( results ).isEmpty();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "join" );
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( " left join " );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsNullInWhereClause3() {
|
public void testIsNullInWhereClause3() {
|
||||||
inTransaction(
|
inTransaction(
|
||||||
|
@ -225,8 +247,22 @@ public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase {
|
||||||
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
// could physically be a join or exists sub-query
|
// could physically be a join or exists sub-query
|
||||||
assertThat( inspector.getSqlQueries().get( 0 ) )
|
assertThat( inspector.getSqlQueries().get( 0 ) )
|
||||||
.matches( (sql) -> sql.contains( "left join" ) || sql.contains( "where exists" ) );
|
.matches( (sql) -> sql.contains( "left join" ) || sql.contains( "not exists" ) );
|
||||||
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( ".id is null" );
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Jira( "https://hibernate.atlassian.net/browse/HHH-17384" )
|
||||||
|
public void testDeleteAdditionalPredicate() {
|
||||||
|
inspector.clear();
|
||||||
|
|
||||||
|
inTransaction( (entityManager) -> {
|
||||||
|
entityManager.createQuery( "delete from Person p where p.account is null and p.lazyAccount.code <>'aaa'" ).executeUpdate();
|
||||||
|
|
||||||
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
|
// could physically be a join or exists sub-query
|
||||||
|
assertThat( inspector.getSqlQueries().get( 0 ) )
|
||||||
|
.matches( (sql) -> sql.contains( "left join" ) || sql.contains( "not exists" ) );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,8 +276,7 @@ public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase {
|
||||||
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
assertThat( inspector.getSqlQueries() ).hasSize( 1 );
|
||||||
// could physically be a join or exists sub-query
|
// could physically be a join or exists sub-query
|
||||||
assertThat( inspector.getSqlQueries().get( 0 ) )
|
assertThat( inspector.getSqlQueries().get( 0 ) )
|
||||||
.matches( (sql) -> sql.contains( "left join" ) || sql.contains( "where exists" ) );
|
.matches( (sql) -> sql.contains( "left join" ) || sql.contains( "not exists" ) );
|
||||||
assertThat( inspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( ".id is null" );
|
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +306,9 @@ public class IsNullAndNotFoundTest extends BaseNonConfigCoreFunctionalTestCase {
|
||||||
@NotFound(action = NotFoundAction.IGNORE)
|
@NotFound(action = NotFoundAction.IGNORE)
|
||||||
private Account account;
|
private Account account;
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
private Account lazyAccount;
|
||||||
|
|
||||||
Person() {
|
Person() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue