HHH-16191 change @NotFound semantic, do not force a join but trigger a subsequent select

This commit is contained in:
Marco Belladelli 2023-02-21 18:11:41 +01:00 committed by Christian Beikov
parent 88a5fd4d28
commit 10bfcabee2
15 changed files with 341 additions and 66 deletions

View File

@ -297,7 +297,7 @@ public class ToOneAttributeMapping
} }
} }
isOptional = ( (ManyToOne) bootValue ).isIgnoreNotFound(); isOptional = ( (ManyToOne) bootValue ).isIgnoreNotFound();
isInternalLoadNullable = ( isNullable && bootValue.isForeignKeyEnabled() ) || notFoundAction == NotFoundAction.IGNORE; isInternalLoadNullable = ( isNullable && bootValue.isForeignKeyEnabled() ) || hasNotFoundAction();
} }
else { else {
assert bootValue instanceof OneToOne; assert bootValue instanceof OneToOne;
@ -1300,14 +1300,13 @@ public class ToOneAttributeMapping
&& parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() ); && parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() );
/* /*
In case of @NotFound we are going to add fetch for the `fetchablePath` only if there is not already a `TableGroupJoin`. In case of selected we are going to add a fetch for the `fetchablePath` only if there is not already a `TableGroupJoin`.
e.g. given : e.g. given :
public static class EntityA { public static class EntityA {
... ...
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.EAGER)
@NotFound(action = NotFoundAction.IGNORE)
private EntityB entityB; private EntityB entityB;
} }
@ -1324,7 +1323,7 @@ public class ToOneAttributeMapping
having the left join we don't want to add an extra implicit join that will be translated into an SQL inner join (see HHH-15342) having the left join we don't want to add an extra implicit join that will be translated into an SQL inner join (see HHH-15342)
*/ */
if ( fetchTiming == FetchTiming.IMMEDIATE && selected || hasNotFoundAction() ) { if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) {
final TableGroup tableGroup = determineTableGroupForFetch( final TableGroup tableGroup = determineTableGroupForFetch(
fetchablePath, fetchablePath,
fetchParent, fetchParent,
@ -1336,9 +1335,11 @@ public class ToOneAttributeMapping
return withRegisteredAssociationKeys( return withRegisteredAssociationKeys(
() -> { () -> {
final DomainResult<?> keyResult; DomainResult<?> keyResult = null;
if ( notFoundAction != null ) {
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
// If the key side is non-nullable we also need to add the keyResult
// to be able to manually check invalid foreign key references
if ( notFoundAction != null || !isInternalLoadNullable ) {
keyResult = foreignKeyDescriptor.createKeyDomainResult( keyResult = foreignKeyDescriptor.createKeyDomainResult(
fetchablePath, fetchablePath,
parentTableGroup, parentTableGroup,
@ -1346,7 +1347,9 @@ public class ToOneAttributeMapping
creationState creationState
); );
} }
else { }
else if ( notFoundAction != null ) {
// For the target side only add keyResult when a not-found action is present
keyResult = foreignKeyDescriptor.createTargetDomainResult( keyResult = foreignKeyDescriptor.createTargetDomainResult(
fetchablePath, fetchablePath,
parentTableGroup, parentTableGroup,
@ -1354,17 +1357,14 @@ public class ToOneAttributeMapping
creationState creationState
); );
} }
}
else {
keyResult = null;
}
return new EntityFetchJoinedImpl( return new EntityFetchJoinedImpl(
fetchParent, fetchParent,
this, this,
tableGroup, tableGroup,
keyResult, keyResult,
fetchablePath,creationState fetchablePath,
creationState
); );
}, },
creationState creationState
@ -1414,7 +1414,8 @@ public class ToOneAttributeMapping
); );
final boolean selectByUniqueKey = isSelectByUniqueKey( side ); final boolean selectByUniqueKey = isSelectByUniqueKey( side );
if ( fetchTiming == FetchTiming.IMMEDIATE ) { // Consider all associations annotated with @NotFound as EAGER
if ( fetchTiming == FetchTiming.IMMEDIATE || hasNotFoundAction() ) {
return new EntityFetchSelectImpl( return new EntityFetchSelectImpl(
fetchParent, fetchParent,
this, this,

View File

@ -77,11 +77,11 @@ public class EntityJoinedFetchInitializer extends AbstractEntityInitializer {
// need to also look at the foreign-key value column to check // need to also look at the foreign-key value column to check
// for a dangling foreign-key // for a dangling foreign-key
if ( notFoundAction != null && keyAssembler != null ) { if ( keyAssembler != null ) {
final Object fkKeyValue = keyAssembler.assemble( rowProcessingState ); final Object fkKeyValue = keyAssembler.assemble( rowProcessingState );
if ( fkKeyValue != null ) { if ( fkKeyValue != null ) {
if ( isMissing() ) { if ( isMissing() ) {
if ( notFoundAction == NotFoundAction.EXCEPTION ) { if ( notFoundAction != NotFoundAction.IGNORE ) {
throw new FetchNotFoundException( throw new FetchNotFoundException(
referencedFetchable.getEntityMappingType().getEntityName(), referencedFetchable.getEntityMappingType().getEntityName(),
fkKeyValue fkKeyValue

View File

@ -115,9 +115,8 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts; final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 1 SQL statement with a join executed // there should be 5 SQL statements executed
assertThat( paramterCounts ).hasSize( 1 ); assertThat( paramterCounts ).hasSize( 5 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) { for ( int i = 0; i < NUMBER_OF_EMPLOYEES; i++ ) {
@ -158,9 +157,8 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest {
final List<Integer> paramterCounts = statementInspector.parameterCounts; final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 1 SQL statement with a join executed // there should be 8 SQL statements executed
assertThat( paramterCounts ).hasSize( 1 ); assertThat( paramterCounts ).hasSize( 8 );
assertThat( paramterCounts.get( 0 ) ).isEqualTo( 0 );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() ); assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );

View File

@ -87,8 +87,8 @@ public class LazyNotFoundOneToOneTest extends BaseCoreFunctionalTestCase {
// `@NotFound` forces EAGER join fetching // `@NotFound` forces EAGER join fetching
assertThat( sqlInterceptor.getQueryCount() ). assertThat( sqlInterceptor.getQueryCount() ).
describedAs( "Expecting 1 query (w/ join) due to `@NotFound`" ) describedAs( "Expecting 2 queries due to `@NotFound`" )
.isEqualTo( 1 ); .isEqualTo( 2 );
assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) ) assertThat( Hibernate.isPropertyInitialized( user, "lazy" ) )
.describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" ) .describedAs( "Expecting `User#lazy` to be eagerly fetched due to `@NotFound`" )
.isTrue(); .isTrue();

View File

@ -76,8 +76,7 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
} }
} ); } );
// not-found associations are always join-fetched, so we should // we should get `NUMBER_OF_ENTITIES` queries
// get `NUMBER_OF_ENTITIES` queries
assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() ); assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
} }
@ -94,9 +93,8 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
} }
} ); } );
// not-found associations are always join-fetched, so we should // we should get `NUMBER_OF_ENTITIES` queries
// get `NUMBER_OF_ENTITIES` queries assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
assertThat( statistics.getPrepareStatementCount() ).isEqualTo( NUMBER_OF_ENTITIES );
} }
@Test @Test
@ -187,7 +185,7 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
private String name; private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "employer_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "employer_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
@NotFound(action=NotFoundAction.IGNORE) @NotFound(action=NotFoundAction.IGNORE)
private Employer employer; private Employer employer;

View File

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

View File

@ -0,0 +1,217 @@
/*
* 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
*/
/*
* 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.bytecode.enhancement.lazy.proxy;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.EnhancementOptions;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import jakarta.persistence.CascadeType;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* @author Andrea Boriero
* @author Gail Badner
*/
@TestForIssue(jiraKey = "HHH-11147")
@RunWith(BytecodeEnhancerRunner.class)
@EnhancementOptions(lazyLoading = true)
public class LoadANonExistingNotFoundLazyBatchEntityTest extends BaseNonConfigCoreFunctionalTestCase {
private static final int NUMBER_OF_ENTITIES = 20;
@Test
@TestForIssue(jiraKey = "HHH-11147")
public void loadEntityWithNotFoundAssociation() {
final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear();
inTransaction( (session) -> {
List<Employee> employees = new ArrayList<>( NUMBER_OF_ENTITIES );
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
employees.add( session.load( Employee.class, i + 1 ) );
}
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
Hibernate.initialize( employees.get( i ) );
assertNull( employees.get( i ).employer );
}
} );
// A "not found" association cannot be batch fetched because
// Employee#employer must be initialized immediately.
// Enhanced proxies (and HibernateProxy objects) should never be created
// for a "not found" association.
assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
}
@Test
@TestForIssue(jiraKey = "HHH-11147")
public void getEntityWithNotFoundAssociation() {
final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear();
inTransaction( (session) -> {
for ( int i = 0 ; i < NUMBER_OF_ENTITIES ; i++ ) {
Employee employee = session.get( Employee.class, i + 1 );
assertNull( employee.employer );
}
} );
// A "not found" association cannot be batch fetched because
// Employee#employer must be initialized immediately.
// Enhanced proxies (and HibernateProxy objects) should never be created
// for a "not found" association.
assertEquals( 2 * NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
}
@Test
@TestForIssue(jiraKey = "HHH-11147")
public void updateNotFoundAssociationWithNew() {
final StatisticsImplementor statistics = sessionFactory().getStatistics();
statistics.clear();
inTransaction( (session) -> {
for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) {
Employee employee = session.get( Employee.class, i + 1 );
Employer employer = new Employer();
employer.id = 2 * employee.id;
employer.name = "Employer #" + employer.id;
employee.employer = employer;
}
} );
inTransaction( (session) -> {
for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) {
Employee employee = session.get( Employee.class, i + 1 );
assertTrue( Hibernate.isInitialized( employee.employer ) );
assertEquals( employee.id * 2, employee.employer.id );
assertEquals( "Employer #" + employee.employer.id, employee.employer.name );
}
} );
}
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Employee.class,
Employer.class
};
}
@Before
public void setUpData() {
doInHibernate(
this::sessionFactory, session -> {
for ( int i = 0; i < NUMBER_OF_ENTITIES; i++ ) {
final Employee employee = new Employee();
employee.id = i + 1;
employee.name = "Employee #" + employee.id;
session.persist( employee );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
// Add "not found" associations
session.createNativeQuery( "update Employee set employer_id = id" ).executeUpdate();
}
);
}
@After
public void cleanupDate() {
doInHibernate(
this::sessionFactory, session -> {
session.createQuery( "delete from Employee" ).executeUpdate();
session.createQuery( "delete from Employer" ).executeUpdate();
}
);
}
@Override
protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
super.configureStandardServiceRegistryBuilder( ssrb );
ssrb.applySetting( AvailableSettings.FORMAT_SQL, "false" );
ssrb.applySetting( AvailableSettings.GENERATE_STATISTICS, "true" );
}
@Override
protected void configureSessionFactoryBuilder(SessionFactoryBuilder sfb) {
super.configureSessionFactoryBuilder( sfb );
sfb.applyStatisticsSupport( true );
sfb.applySecondLevelCacheSupport( false );
sfb.applyQueryCacheSupport( false );
}
@Entity(name = "Employee")
public static class Employee {
@Id
private int id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "employer_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
@NotFound(action=NotFoundAction.IGNORE)
private Employer employer;
}
@Entity(name = "Employer")
@BatchSize(size = 10)
public static class Employer {
@Id
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -7,6 +7,10 @@
package org.hibernate.orm.test.inheritance; package org.hibernate.orm.test.inheritance;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -303,6 +307,7 @@ public class TransientOverrideAsPersistentMappedSuperclass {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -7,6 +7,10 @@
package org.hibernate.orm.test.inheritance; package org.hibernate.orm.test.inheritance;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -296,6 +300,7 @@ public class TransientOverrideAsPersistentSingleTable {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -8,6 +8,10 @@ package org.hibernate.orm.test.inheritance;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -299,6 +303,7 @@ public class TransientOverrideAsPersistentTablePerClass {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -196,6 +196,40 @@ public class EagerProxyNotFoundTest {
} }
} }
@Test
public void testGetEmployeeWithNotExistingAssociation(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final Employee employee = new Employee();
employee.id = 1;
session.persist( employee );
session.flush();
session.createNativeQuery( "update Employee set locationId = 3 where id = 1" )
.executeUpdate();
} );
try {
scope.inTransaction( session -> session.get( Employee.class, 1 ) );
fail( "EntityNotFoundException should have been thrown because Employee.location is not found " +
"and is not mapped with @NotFound(IGNORE)" );
}
catch (EntityNotFoundException expected) {
}
// also test explicit join
try {
scope.inTransaction( session -> session.createQuery(
"from Employee e left join e.location ",
Employee.class
).getSingleResult() );
fail( "EntityNotFoundException should have been thrown because Employee.location is not found " +
"and is not mapped with @NotFound(IGNORE)" );
}
catch (EntityNotFoundException expected) {
}
}
@Test @Test
public void testExistingProxyWithNoAssociation(SessionFactoryScope scope) { public void testExistingProxyWithNoAssociation(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction(

View File

@ -9,20 +9,12 @@ package org.hibernate.orm.test.notfound;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
import org.hibernate.query.SemanticException;
import org.hibernate.query.sqm.ParsingException; import org.hibernate.query.sqm.ParsingException;
import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel; 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.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScope;
@ -30,6 +22,12 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
@ -44,11 +42,6 @@ public class FkRefTests {
@Test @Test
@JiraKey( "HHH-15099" ) @JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" ) @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) { public void testSimplePredicateUse(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
@ -160,11 +153,6 @@ public class FkRefTests {
@Test @Test
@JiraKey( "HHH-15099" ) @JiraKey( "HHH-15099" )
@JiraKey( "HHH-15106" ) @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) { public void testNullnessPredicateUse2(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();

View File

@ -62,8 +62,7 @@ public class NotFoundAndSelectJoinTest {
assertThat( person ).isNotNull(); assertThat( person ).isNotNull();
assertThat( Hibernate.isInitialized( person ) ); assertThat( Hibernate.isInitialized( person ) );
assertThat( statementInspector.getSqlQueries().size() ).isEqualTo( 1 ); assertThat( statementInspector.getSqlQueries().size() ).isEqualTo( 2 );
assertThat( statementInspector.getNumberOfJoins( 0 ) ).isEqualTo( 1 );
} }
); );
} }

View File

@ -224,7 +224,7 @@ public class NotFoundExceptionLogicalOneToOneTest {
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList(); final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 ); assertThat( coins ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Coin " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " Currency " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " ); assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
@ -333,7 +333,7 @@ public class NotFoundExceptionLogicalOneToOneTest {
this.name = name; this.name = name;
} }
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.EAGER)
@NotFound(action = NotFoundAction.EXCEPTION) @NotFound(action = NotFoundAction.EXCEPTION)
public Currency getCurrency() { public Currency getCurrency() {
return currency; return currency;

View File

@ -4,12 +4,14 @@
* License: GNU Lesser General Public License (LGPL), version 2.1 or later * 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 * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/ */
package org.hibernate.orm.test.query.sql; package org.hibernate.orm.test.query.sql;
import java.util.List; import java.util.List;
import org.hibernate.FetchNotFoundException;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.JiraKey;
@ -24,8 +26,10 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
@ -44,10 +48,16 @@ public class NativeQueryEagerAssociationTest {
final Building building1 = new Building( 1L, "building_1" ); final Building building1 = new Building( 1L, "building_1" );
final Building building2 = new Building( 2L, "building_2" ); final Building building2 = new Building( 2L, "building_2" );
final Building building3 = new Building( 3L, "building_3" ); final Building building3 = new Building( 3L, "building_3" );
final Building building4 = new Building( 4L, "building_4" );
session.persist( building1 ); session.persist( building1 );
session.persist( building2 ); session.persist( building2 );
session.persist( building3 ); session.persist( building3 );
session.persist( new Classroom( 1L, "classroom_1", building1, List.of( building2, building3 ) ) ); session.persist( new Classroom( 1L, "classroom_1", building1, List.of( building2, building3 ) ) );
session.persist( new Classroom( 2L, "classroom_2", building4, null ) );
} );
scope.inTransaction( session -> {
// delete associated entity to trigger @NotFound
session.createMutationQuery( "delete from Building where id = 4L" ).executeUpdate();
} ); } );
} }
@ -62,16 +72,27 @@ public class NativeQueryEagerAssociationTest {
@Test @Test
public void testNativeQuery(SessionFactoryScope scope) { public void testNativeQuery(SessionFactoryScope scope) {
final Classroom result = scope.fromTransaction( final Classroom result = scope.fromTransaction(
session -> session.createNativeQuery( "select id, description, building_id from classroom", Classroom.class ).getSingleResult() session -> session.createNativeQuery( "select * from Classroom where id = 1", Classroom.class )
.getSingleResult()
); );
assertEquals( 1L, result.getId() ); assertEquals( 1L, result.getId() );
assertTrue( Hibernate.isInitialized( result.getBuilding() ) ); assertTrue( Hibernate.isInitialized( result.getBuilding() ) );
assertTrue( Hibernate.isInitialized( result.getAdjacentBuildings() ) ); assertTrue( Hibernate.isInitialized( result.getAdjacentBuildings() ) );
assertEquals( 1L, result.getBuilding().getId() ); assertEquals( 1L, result.getBuilding().getId() );
assertEquals( "building_1", result.getBuilding().getDescription() );
assertEquals( 2, result.getAdjacentBuildings().size() ); assertEquals( 2, result.getAdjacentBuildings().size() );
} }
@Test
public void testNativeQueryNotFound(SessionFactoryScope scope) {
assertThrows( FetchNotFoundException.class, () -> scope.inTransaction(
session -> session.createNativeQuery( "select * from Classroom where id = 2", Classroom.class )
.getSingleResult()
) );
}
@Entity(name = "Building") @Entity(name = "Building")
@Table(name = "Building")
public static class Building { public static class Building {
@Id @Id
private Long id; private Long id;
@ -96,6 +117,7 @@ public class NativeQueryEagerAssociationTest {
} }
@Entity(name = "Classroom") @Entity(name = "Classroom")
@Table(name = "Classroom")
public static class Classroom { public static class Classroom {
@Id @Id
private Long id; private Long id;
@ -103,6 +125,7 @@ public class NativeQueryEagerAssociationTest {
private String description; private String description;
@ManyToOne(fetch = FetchType.EAGER) @ManyToOne(fetch = FetchType.EAGER)
@NotFound(action = NotFoundAction.EXCEPTION)
private Building building; private Building building;
@OneToMany(fetch = FetchType.EAGER) @OneToMany(fetch = FetchType.EAGER)