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 1f4f0c6ec1
commit 7e305df0f8
15 changed files with 341 additions and 66 deletions

View File

@ -297,7 +297,7 @@ public class ToOneAttributeMapping
}
}
isOptional = ( (ManyToOne) bootValue ).isIgnoreNotFound();
isInternalLoadNullable = ( isNullable && bootValue.isForeignKeyEnabled() ) || notFoundAction == NotFoundAction.IGNORE;
isInternalLoadNullable = ( isNullable && bootValue.isForeignKeyEnabled() ) || hasNotFoundAction();
}
else {
assert bootValue instanceof OneToOne;
@ -1300,14 +1300,13 @@ public class ToOneAttributeMapping
&& 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 :
public static class EntityA {
...
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
@ManyToOne(fetch = FetchType.EAGER)
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)
*/
if ( fetchTiming == FetchTiming.IMMEDIATE && selected || hasNotFoundAction() ) {
if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) {
final TableGroup tableGroup = determineTableGroupForFetch(
fetchablePath,
fetchParent,
@ -1336,9 +1335,11 @@ public class ToOneAttributeMapping
return withRegisteredAssociationKeys(
() -> {
final DomainResult<?> keyResult;
if ( notFoundAction != null ) {
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
DomainResult<?> keyResult = null;
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(
fetchablePath,
parentTableGroup,
@ -1346,17 +1347,15 @@ public class ToOneAttributeMapping
creationState
);
}
else {
keyResult = foreignKeyDescriptor.createTargetDomainResult(
fetchablePath,
parentTableGroup,
fetchParent,
creationState
);
}
}
else {
keyResult = null;
else if ( notFoundAction != null ) {
// For the target side only add keyResult when a not-found action is present
keyResult = foreignKeyDescriptor.createTargetDomainResult(
fetchablePath,
parentTableGroup,
fetchParent,
creationState
);
}
return new EntityFetchJoinedImpl(
@ -1364,7 +1363,8 @@ public class ToOneAttributeMapping
this,
tableGroup,
keyResult,
fetchablePath,creationState
fetchablePath,
creationState
);
},
creationState
@ -1414,7 +1414,8 @@ public class ToOneAttributeMapping
);
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(
fetchParent,
this,

View File

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

View File

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

View File

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

View File

@ -76,8 +76,7 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
}
} );
// not-found associations are always join-fetched, so we should
// get `NUMBER_OF_ENTITIES` queries
// we should get `NUMBER_OF_ENTITIES` queries
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
// get `NUMBER_OF_ENTITIES` queries
assertThat( statistics.getPrepareStatementCount() ).isEqualTo( NUMBER_OF_ENTITIES );
// we should get `NUMBER_OF_ENTITIES` queries
assertEquals( NUMBER_OF_ENTITIES, statistics.getPrepareStatementCount() );
}
@Test
@ -187,7 +185,7 @@ public class LoadANonExistingNotFoundBatchEntityTest extends BaseNonConfigCoreFu
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))
@NotFound(action=NotFoundAction.IGNORE)
private Employer employer;

View File

@ -67,9 +67,10 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
}
);
// not-found associations are always join-fetched, so we should
// get 1 query for the Employee with join
assertEquals( 1, statistics.getPrepareStatementCount() );
// The Employee#employer must be initialized immediately because
// enhanced proxies (and HibernateProxy objects) should never be created
// for a "not found" association.
assertEquals( 2, statistics.getPrepareStatementCount() );
}
@Test
@ -85,9 +86,10 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
}
);
// not-found associations are always join-fetched, so we should
// get 1 query for the Employee with join
assertEquals( 1, statistics.getPrepareStatementCount() );
// The Employee#employer must be initialized immediately because
// enhanced proxies (and HibernateProxy objects) should never be created
// for a "not found" association.
assertEquals( 2, statistics.getPrepareStatementCount() );
}
@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;
import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn;
@ -303,6 +307,7 @@ public class TransientOverrideAsPersistentMappedSuperclass {
// Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group.
@ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() {
return group;

View File

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

View File

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

View File

@ -9,20 +9,12 @@ package org.hibernate.orm.test.notfound;
import java.io.Serializable;
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.NotFoundAction;
import org.hibernate.query.SemanticException;
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;
@ -30,6 +22,12 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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;
/**
@ -44,11 +42,6 @@ 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();
@ -160,11 +153,6 @@ 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 testNullnessPredicateUse2(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();

View File

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

View File

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

View File

@ -4,12 +4,14 @@
* 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.query.sql;
import java.util.List;
import org.hibernate.FetchNotFoundException;
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.JiraKey;
@ -24,8 +26,10 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
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 building2 = new Building( 2L, "building_2" );
final Building building3 = new Building( 3L, "building_3" );
final Building building4 = new Building( 4L, "building_4" );
session.persist( building1 );
session.persist( building2 );
session.persist( 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
public void testNativeQuery(SessionFactoryScope scope) {
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() );
assertTrue( Hibernate.isInitialized( result.getBuilding() ) );
assertTrue( Hibernate.isInitialized( result.getAdjacentBuildings() ) );
assertEquals( 1L, result.getBuilding().getId() );
assertEquals( "building_1", result.getBuilding().getDescription() );
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")
@Table(name = "Building")
public static class Building {
@Id
private Long id;
@ -96,6 +117,7 @@ public class NativeQueryEagerAssociationTest {
}
@Entity(name = "Classroom")
@Table(name = "Classroom")
public static class Classroom {
@Id
private Long id;
@ -103,6 +125,7 @@ public class NativeQueryEagerAssociationTest {
private String description;
@ManyToOne(fetch = FetchType.EAGER)
@NotFound(action = NotFoundAction.EXCEPTION)
private Building building;
@OneToMany(fetch = FetchType.EAGER)