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

- Tests
This commit is contained in:
Steve Ebersole 2022-03-03 16:53:58 -06:00
parent bdf8b2fc2e
commit de97e8e1a4
4 changed files with 117 additions and 54 deletions

View File

@ -7,6 +7,7 @@
package org.hibernate.orm.test.notfound.exception;
import java.io.Serializable;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
@ -17,6 +18,7 @@ import org.hibernate.ObjectNotFoundException;
import org.hibernate.annotations.NotFound;
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;
@ -42,13 +44,13 @@ import static org.junit.jupiter.api.Assertions.fail;
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = { NotFoundExceptionLogicalOneToOneTest.Coin.class, NotFoundExceptionLogicalOneToOneTest.Currency.class } )
@SessionFactory
@SessionFactory( useCollectingStatementInspector = true )
public class NotFoundExceptionLogicalOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
public void testProxy(SessionFactoryScope scope) {
// test handling of a proxy for the missing Coin
scope.inTransaction( (session) -> {
// the non-existent Child
final Currency proxy = session.byId( Currency.class ).getReference( 1 );
try {
Hibernate.initialize( proxy );
@ -63,11 +65,20 @@ public class NotFoundExceptionLogicalOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "We return a proxy here for `Coin#currency`, which violates NOTE #1. The exception happens " +
"when we reference the proxy, but thats not correct"
)
@FailureExpected( reason = "We return a proxy here for `Coin#currency`, which violates NOTE #1." )
public void testGet(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
session.get( Coin.class, 2 );
// here we assume join, but this could use subsequent-select instead
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
scope.inTransaction( (session) -> {
try {
session.get( Coin.class, 1 );
@ -82,15 +93,43 @@ public class NotFoundExceptionLogicalOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "We return a proxy here for `Coin#currency`, which violates NOTE #1. The exception happens " +
"when we reference the proxy, but thats not correct"
)
@FailureExpected( reason = "We return a proxy for `Coin#currency`, which violates NOTE #1." )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
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 ).isEmpty();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
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( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
}
@Test
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "We return a proxy for `Coin#currency`, which violates NOTE #1." )
public void testQueryOwnerSelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.id = 1";
try {
session.createQuery( hql, Coin.class ).getResultList();
//noinspection unused (debugging)
final Coin coin = session.createQuery( hql, Coin.class ).uniqueResult();
fail( "Expecting ObjectNotFoundException for broken fk" );
}
catch (ObjectNotFoundException expected) {
@ -98,25 +137,12 @@ public class NotFoundExceptionLogicalOneToOneTest {
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
} );
}
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "We return a proxy here for `Coin#currency`, which violates NOTE #1. The exception happens " +
"when we reference the proxy, but thats not correct"
)
public void testQueryOwnerSelection(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c";
try {
session.createQuery( hql, Coin.class ).getResultList();
fail( "Expecting ObjectNotFoundException for broken fk" );
}
catch (ObjectNotFoundException expected) {
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
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();
} );
}
@ -125,9 +151,13 @@ public class NotFoundExceptionLogicalOneToOneTest {
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) -> {
@ -138,7 +168,8 @@ public class NotFoundExceptionLogicalOneToOneTest {
@AfterEach
public void cleanupTest(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

@ -7,6 +7,7 @@
package org.hibernate.orm.test.notfound.exception;
import java.io.Serializable;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
@ -66,8 +67,7 @@ public class NotFoundExceptionManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "ObjectNotFoundException is thrown but caught and null is returned - see " +
"org.hibernate.internal.SessionImpl.IdentifierLoadAccessImpl#doLoad"
reason = "ObjectNotFoundException is thrown, but caught in `IdentifierLoadAccessImpl#doLoad` and null returned instead"
)
public void testGet(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
@ -94,26 +94,28 @@ public class NotFoundExceptionManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "EntityNotFoundException thrown rather than ObjectNotFoundException; " +
"ObjectNotFoundException is thrown but caught and then converted to EntityNotFoundException"
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) -> {
final String hql = "select c from Coin c where c.currency.id = 1";
try {
final String hql = "select c from Coin c where c.currency.id = 1";
session.createQuery( hql, Coin.class ).getResultList();
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( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
} );
}
@ -121,27 +123,28 @@ public class NotFoundExceptionManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected(
reason = "EntityNotFoundException thrown rather than ObjectNotFoundException; " +
"ObjectNotFoundException is thrown but caught and then converted to EntityNotFoundException"
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 testQueryOwnerSelection(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c";
final String hql = "select c from Coin c where c.id = 1";
try {
session.createQuery( hql, Coin.class ).getResultList();
fail( "Expecting ObjectNotFoundException for broken fk" );
}
catch (ObjectNotFoundException expected) {
assertThat( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
// 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( expected.getEntityName() ).isEqualTo( Currency.class.getName() );
assertThat( expected.getIdentifier() ).isEqualTo( 1 );
}
} );
}
@ -156,7 +159,7 @@ public class NotFoundExceptionManyToOneTest {
// - 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";
final String hql = "select c.currency from Coin c where c.id = 1";
try {
session.createQuery( hql, Currency.class ).getResultList();
fail( "Expecting ObjectNotFoundException for broken fk" );
@ -173,9 +176,13 @@ public class NotFoundExceptionManyToOneTest {
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) -> {
@ -184,9 +191,10 @@ public class NotFoundExceptionManyToOneTest {
}
@AfterEach
protected void dropTestData(SessionFactoryScope scope) {
public void cleanupTest(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

@ -12,6 +12,7 @@ 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;
@ -83,7 +84,6 @@ public class NotFoundIgnoreManyToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "Bad results due to cross-join" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -91,8 +91,7 @@ public class NotFoundIgnoreManyToOneTest {
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.id = 1";
final List<Coin> coins = session.createSelectionQuery( 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 );
@ -134,6 +133,22 @@ public class NotFoundIgnoreManyToOneTest {
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();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
} );
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";
session.createQuery( hql, Currency.class ).getResultList();

View File

@ -12,6 +12,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Tuple;
import org.hibernate.Hibernate;
import org.hibernate.ObjectNotFoundException;
@ -83,7 +84,7 @@ public class NotFoundIgnoreOneToOneTest {
@Test
@JiraKey( "HHH-15060" )
@FailureExpected( reason = "Bad results due to cross-join" )
@FailureExpected( reason = "Bad results due to join" )
public void testQueryImplicitPathDereferencePredicate(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
@ -127,11 +128,19 @@ public class NotFoundIgnoreOneToOneTest {
@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 join; & the select w/ join is executed twice for some yet-unknown reason" )
public void testQueryAssociationSelection(SessionFactoryScope scope) {
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();
} );
scope.inTransaction( (session) -> {
final String hql = "select c.currency from Coin c";
session.createQuery( hql, Currency.class ).getResultList();
final List<Currency> currencies = session.createQuery( hql, Currency.class ).getResultList();
assertThat( currencies ).hasSize( 1 );
assertThat( currencies.get( 0 ) ).isNull();