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

HHH-15106 - fk() SQM function
This commit is contained in:
Steve Ebersole 2022-03-08 06:20:37 -06:00
parent 362b4c0ac7
commit 0af7ed353a
7 changed files with 397 additions and 130 deletions

View File

@ -28,6 +28,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import jakarta.persistence.TemporalType;
import org.hibernate.HibernateException;
import org.hibernate.Internal;
@ -374,8 +375,6 @@ import org.hibernate.usertype.UserVersionType;
import org.jboss.logging.Logger;
import jakarta.persistence.TemporalType;
import static org.hibernate.internal.util.NullnessHelper.coalesceSuppliedValues;
import static org.hibernate.query.sqm.BinaryArithmeticOperator.ADD;
import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY;
@ -2880,6 +2879,27 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
else if ( parentPath instanceof SqmFrom<?, ?> ) {
registerTreatUsage( (SqmFrom<?, ?>) parentPath, tableGroup );
}
if ( getCurrentClauseStack().getCurrent() != Clause.SELECT
&& parentPath.getParentPath() != null
&& tableGroup.getModelPart() instanceof ToOneAttributeMapping ) {
// we need to handle the case of an implicit path involving a to-one
// association with not-found mapping where that path has been previously
// joined using left. typically, this indicates that the to-one is being
// fetched - the fetch would use a left-join. however, since the path is
// used outside the select-clause also, we need to force the join to be inner
final ToOneAttributeMapping toOneMapping = (ToOneAttributeMapping) tableGroup.getModelPart();
if ( toOneMapping.hasNotFoundAction() ) {
final NavigablePath parentParentPath = parentPath.getParentPath().getNavigablePath();
final TableGroup parentParentTableGroup = fromClauseIndex.findTableGroup( parentParentPath );
parentParentTableGroup.visitTableGroupJoins( (join) -> {
if ( join.getNavigablePath().equals( parentPath.getNavigablePath() ) ) {
join.setJoinType( SqlAstJoinType.INNER );
}
} );
}
}
return tableGroup;
}

View File

@ -10,6 +10,7 @@ import org.hibernate.query.spi.NavigablePath;
import org.hibernate.query.sqm.sql.internal.DomainResultProducer;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.SqlTreeCreationLogger;
import org.hibernate.sql.ast.spi.SqlAstTreeHelper;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.predicate.Predicate;
@ -21,36 +22,46 @@ import org.hibernate.sql.results.graph.DomainResultCreationState;
*/
public class TableGroupJoin implements TableJoin, DomainResultProducer {
private final NavigablePath navigablePath;
private final SqlAstJoinType sqlAstJoinType;
private final TableGroup joinedGroup;
private SqlAstJoinType joinType;
private Predicate predicate;
public TableGroupJoin(
NavigablePath navigablePath,
SqlAstJoinType sqlAstJoinType,
SqlAstJoinType joinType,
TableGroup joinedGroup) {
this( navigablePath, sqlAstJoinType, joinedGroup, null );
this( navigablePath, joinType, joinedGroup, null );
}
public TableGroupJoin(
NavigablePath navigablePath,
SqlAstJoinType sqlAstJoinType,
SqlAstJoinType joinType,
TableGroup joinedGroup,
Predicate predicate) {
assert !joinedGroup.isLateral() || ( sqlAstJoinType == SqlAstJoinType.INNER
|| sqlAstJoinType == SqlAstJoinType.LEFT
|| sqlAstJoinType == SqlAstJoinType.CROSS )
assert !joinedGroup.isLateral() || ( joinType == SqlAstJoinType.INNER
|| joinType == SqlAstJoinType.LEFT
|| joinType == SqlAstJoinType.CROSS )
: "Lateral is only allowed with inner, left or cross joins";
this.navigablePath = navigablePath;
this.sqlAstJoinType = sqlAstJoinType;
this.joinType = joinType;
this.joinedGroup = joinedGroup;
this.predicate = predicate;
}
@Override
public SqlAstJoinType getJoinType() {
return sqlAstJoinType;
return joinType;
}
public void setJoinType(SqlAstJoinType joinType) {
SqlTreeCreationLogger.LOGGER.debugf(
"Adjusting join-type for TableGroupJoin(%s) : %s -> %s",
navigablePath,
this.joinType,
joinType
);
this.joinType = joinType;
}
public TableGroup getJoinedGroup() {

View File

@ -22,6 +22,7 @@ import org.hibernate.query.sqm.ParsingException;
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;
@ -42,6 +43,12 @@ public class FkRefTests {
@Test
@JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" )
@FailureExpected(
reason = "Coin is selected and so its currency needs to be fetched. At the " +
"moment, that fetch always happens via a join-fetch. Ideally we'd support " +
"loading these via subsequent-select also"
)
public void testSimplePredicateUse(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -83,12 +90,13 @@ public class FkRefTests {
}
/**
* Baseline test for {@link #testNullnessPredicateUse}. Here we use the
* Baseline test for {@link #testNullnessPredicateUse2}. Here we use the
* normal "target" reference, which for a not-found mapping should trigger
* a join to the association table and use the fk-target column
*/
@Test
@JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" )
public void testNullnessPredicateUseBaseline(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -103,17 +111,61 @@ public class FkRefTests {
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" )
public void testNullnessPredicateUse1(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// there is one Coin (id=3) which has a null currency_fk
scope.inTransaction( (session) -> {
final String hql = "select c.id from Coin c where fk(c.currency) is null";
final List<Integer> coinIds = session.createQuery( hql, Integer.class ).getResultList();
assertThat( coinIds ).hasSize( 1 );
assertThat( coinIds.get( 0 ) ).isNotNull();
assertThat( coinIds.get( 0 ) ).isEqualTo( 3 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
statementInspector.clear();
// check using `currency` as a naked "property-ref"
scope.inTransaction( (session) -> {
final String hql = "select c.id from Coin c where fk(currency) is null";
final List<Integer> coinIds = session.createQuery( hql, Integer.class ).getResultList();
assertThat( coinIds ).hasSize( 1 );
assertThat( coinIds.get( 0 ) ).isNotNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
}
/**
* It is not ideal that we render the join for these. Need to come back and address that
* It is not ideal that we render the join for these.
*
* Ideally we'd perform a subsequent-select, not sure if that is feasible as it requires
* understanding the overall query structure.
*
* Compare with {@link #testNullnessPredicateUse1}. There, because we perform a scalar select,
* the currency does not need to be fetched. So it works there
*/
@Test
@JiraKey( "HHH-15099" )
public void testNullnessPredicateUse(SessionFactoryScope scope) {
@JiraKey( "HHH-15106" )
@FailureExpected(
reason = "Coin is selected and so its currency needs to be fetched. At the " +
"moment, that fetch always happens via a join-fetch. Ideally we'd support " +
"loading these via subsequent-select also"
)
public void testNullnessPredicateUse2(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -124,7 +176,6 @@ public class FkRefTests {
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getId() ).isEqualTo( 3 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
@ -139,7 +190,6 @@ public class FkRefTests {
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getId() ).isEqualTo( 3 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
@ -148,6 +198,7 @@ public class FkRefTests {
@Test
@JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" )
public void testFkRefDereferenceNotAllowed(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -190,9 +241,9 @@ public class FkRefTests {
session.persist( noCurrency );
} );
// scope.inTransaction( (session) -> {
// session.createQuery( "delete Currency where id = 1" ).executeUpdate();
// } );
scope.inTransaction( (session) -> {
session.createQuery( "delete Currency where id = 1" ).executeUpdate();
} );
}
@AfterEach

View File

@ -68,12 +68,12 @@ public class NotFoundExceptionLogicalOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testProxyCoin(SessionFactoryScope scope) {
// test handling of a proxy for the missing Coin
// test handling of a proxy for the Coin with the missing Currency
scope.inTransaction( (session) -> {
final Coin proxy = session.byId( Coin.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
Assertions.fail( "Expecting ObjectNotFoundException" );
Assertions.fail( "Expecting FetchNotFoundException" );
}
catch (FetchNotFoundException expected) {
assertThat( expected.getEntityName() ).endsWith( "Currency" );
@ -132,7 +132,7 @@ public class NotFoundExceptionLogicalOneToOneTest {
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 " );
}
@ -148,15 +148,15 @@ public class NotFoundExceptionLogicalOneToOneTest {
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 );
final String hql = "select c.id from Coin c where c.currency.id = 2";
final List<Integer> coinIds = session.createQuery( hql, Integer.class ).getResultList();
assertThat( coinIds ).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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@ -172,29 +172,29 @@ public class NotFoundExceptionLogicalOneToOneTest {
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 );
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) -> {
// 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 String hql = "select c from Coin c join fetch c.currency c2 where c2.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@ -213,7 +213,7 @@ public class NotFoundExceptionLogicalOneToOneTest {
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 " );
} );
@ -228,7 +228,8 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}

View File

@ -97,6 +97,7 @@ public class NotFoundExceptionManyToOneTest {
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 " );
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
@ -108,7 +109,7 @@ 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
* join. Because the
*/
@Test
@JiraKey( "HHH-15060" )
@ -122,11 +123,15 @@ public class NotFoundExceptionManyToOneTest {
assertThat( coins ).isEmpty();
} );
// the problem, as with the rest of the failures here, is that the fetch
// causes a left join to be used. The where-clause path is either
// 1) not processed first
// 2) not processed properly
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 " );
}
@ -142,15 +147,18 @@ public class NotFoundExceptionManyToOneTest {
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 );
final String hql = "select c.id from Coin c where c.currency.id = 2";
final List<Integer> coinIds = session.createQuery( hql, Integer.class ).getResultList();
assertThat( coinIds ).hasSize( 1 );
// this form works because we do not fetch the currency since
// we select just the Coin id
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 " );
} );
}
@ -166,25 +174,20 @@ public class NotFoundExceptionManyToOneTest {
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 );
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) -> {
// 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 );
@ -198,6 +201,11 @@ public class NotFoundExceptionManyToOneTest {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// the problem, as with the rest of the failures here, is that the fetch
// causes a left join to be used. The where-clause path is either
// 1) not processed first
// 2) not processed properly
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();
@ -207,7 +215,8 @@ public class NotFoundExceptionManyToOneTest {
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}

View File

@ -12,7 +12,6 @@ import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Tuple;
import org.hibernate.Hibernate;
import org.hibernate.ObjectNotFoundException;
@ -21,7 +20,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;
@ -49,10 +47,22 @@ public class NotFoundIgnoreManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) {
public void testProxyCoin(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// the non-existent Child
// - this is the one valid deviation from treating the broken fk as null
// Coin#1 has the broken fk
final Coin proxy = session.byId( Coin.class ).getReference( 1 );
assertThat( proxy ).isNotNull();
Hibernate.initialize( proxy );
assertThat( Hibernate.isInitialized( proxy ) ).isTrue();
assertThat( proxy.getCurrency() ).isNull();
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testProxyCurrency(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// Currency#1 does not exist
final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
@ -71,6 +81,19 @@ public class NotFoundIgnoreManyToOneTest {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
session.get( Coin.class, 2 );
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();
scope.inTransaction( (session) -> {
final Coin coin = session.get( Coin.class, 1 );
assertThat( coin.getCurrency() ).isNull();
@ -82,6 +105,80 @@ public class NotFoundIgnoreManyToOneTest {
} );
}
@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.createSelectionQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testQueryImplicitPathDereferencePredicateBaseline2(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.id from Coin c where c.currency.id = 1";
final List<Integer> coins = session.createSelectionQuery( hql, Integer.class ).getResultList();
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( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
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) -> {
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 );
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 from Coin c join fetch c.currency c2 where c2.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
@ -93,58 +190,41 @@ public class NotFoundIgnoreManyToOneTest {
final List<Coin> coins = session.createSelectionQuery( hql, Coin.class ).getResultList();
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( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testQueryOwnerSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.id = 1";
final Coin coin = session.createQuery( hql, Coin.class ).uniqueResult();
assertThat( coin ).isNotNull();
assertThat( Hibernate.isPropertyInitialized( coin, "currency" ) ).isTrue();
assertThat( Hibernate.isInitialized( coin.getCurrency() ) ).isTrue();
assertThat( coin.getCurrency() ).isNull();
} );
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c";
final List<Coin> coins = session.createSelectionQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
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 " );
final String hql = "select c from Coin c where c.id = 2";
final Coin coin = session.createQuery( hql, Coin.class ).uniqueResult();
assertThat( Hibernate.isPropertyInitialized( coin, "currency" ) ).isTrue();
assertThat( Hibernate.isInitialized( coin.getCurrency() ) ).isTrue();
} );
}
@Test
@JiraKey( "HHH-15060" )
// @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.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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
statementInspector.clear();
// 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";
final String hql = "select c.currency from Coin c where c.id = 1";
final List<Currency> currencies = session.createSelectionQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 0 );
@ -162,9 +242,13 @@ public class NotFoundIgnoreManyToOneTest {
scope.inTransaction( (session) -> {
Currency euro = new Currency( 1, "Euro" );
Coin fiveC = new Coin( 1, "Five cents", euro );
session.persist( euro );
session.persist( fiveC );
Currency usd = new Currency( 2, "USD" );
Coin penny = new Coin( 2, "Penny", usd );
session.persist( usd );
session.persist( penny );
} );
scope.inTransaction( (session) -> {
@ -175,7 +259,8 @@ public class NotFoundIgnoreManyToOneTest {
@AfterEach
protected void dropTestData(SessionFactoryScope scope) throws Exception {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete Coin where id = 1" ).executeUpdate();
session.createMutationQuery( "delete Coin" ).executeUpdate();
session.createMutationQuery( "delete Currency" ).executeUpdate();
} );
}

View File

@ -48,12 +48,24 @@ public class NotFoundIgnoreOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) {
public void testProxyCoin(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// the non-existent Child
// - this is the one valid deviation from treating the broken fk as null
try {
// Coin#1 has the broken fk
final Coin proxy = session.byId( Coin.class ).getReference( 1 );
assertThat( proxy ).isNotNull();
Hibernate.initialize( proxy );
assertThat( Hibernate.isInitialized( proxy ) ).isTrue();
assertThat( proxy.getCurrency() ).isNull();
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testProxyCurrency(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
// Currency#1 does not exist
final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
Assertions.fail( "Expecting ObjectNotFoundException" );
}
@ -73,7 +85,6 @@ public class NotFoundIgnoreOneToOneTest {
scope.inTransaction( (session) -> {
final Coin coin = session.get( Coin.class, 1 );
assertThat( coin.getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
@ -81,6 +92,89 @@ public class NotFoundIgnoreOneToOneTest {
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
statementInspector.clear();
scope.inTransaction( (session) -> {
final Coin coin = session.get( Coin.class, 1 );
assertThat( coin.getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
}
@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.createSelectionQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@JiraKey( "HHH-15060" )
public void testQueryImplicitPathDereferencePredicateBaseline2(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c.id from Coin c where c.currency.id = 1";
final List<Integer> coins = session.createSelectionQuery( hql, Integer.class ).getResultList();
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( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
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) -> {
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 );
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 from Coin c join fetch c.currency c2 where c2.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@Test
@ -95,10 +189,9 @@ public class NotFoundIgnoreOneToOneTest {
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( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
@ -109,16 +202,19 @@ public class NotFoundIgnoreOneToOneTest {
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
final String hql = "select c from Coin c where c.id = 1";
final Coin coin = session.createQuery( hql, Coin.class ).uniqueResult();
assertThat( coin ).isNotNull();
assertThat( Hibernate.isPropertyInitialized( coin, "currency" ) ).isTrue();
assertThat( Hibernate.isInitialized( coin.getCurrency() ) ).isTrue();
assertThat( coin.getCurrency() ).isNull();
} );
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 " );
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.id = 2";
final Coin coin = session.createQuery( hql, Coin.class ).uniqueResult();
assertThat( Hibernate.isPropertyInitialized( coin, "currency" ) ).isTrue();
assertThat( Hibernate.isInitialized( coin.getCurrency() ) ).isTrue();
} );
}
@ -129,22 +225,11 @@ public class NotFoundIgnoreOneToOneTest {
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( 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();
final String hql = "select c.currency from Coin c where c.id = 1";
final List<Currency> currencies = session.createSelectionQuery( hql, Currency.class ).getResultList();
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 " );
@ -158,9 +243,13 @@ public class NotFoundIgnoreOneToOneTest {
scope.inTransaction( (session) -> {
Currency euro = new Currency( 1, "Euro" );
Coin fiveC = new Coin( 1, "Five cents", euro );
session.persist( euro );
session.persist( fiveC );
Currency usd = new Currency( 2, "USD" );
Coin penny = new Coin( 2, "Penny", usd );
session.persist( usd );
session.persist( penny );
} );
scope.inTransaction( (session) -> {
@ -171,7 +260,8 @@ public class NotFoundIgnoreOneToOneTest {
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete Coin where id = 1" ).executeUpdate();
session.createMutationQuery( "delete Coin" ).executeUpdate();
session.createMutationQuery( "delete Currency" ).executeUpdate();
} );
}