From 10bfcabee2e7673a5d1c448082d56db9825b4bbb Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Tue, 21 Feb 2023 18:11:41 +0100 Subject: [PATCH] HHH-16191 change @NotFound semantic, do not force a join but trigger a subsequent select --- .../internal/ToOneAttributeMapping.java | 41 ++-- .../EntityJoinedFetchInitializer.java | 4 +- ...chFetchNotFoundIgnoreDynamicStyleTest.java | 10 +- .../notfound/LazyNotFoundOneToOneTest.java | 4 +- ...adANonExistingNotFoundBatchEntityTest.java | 10 +- .../LoadANonExistingNotFoundEntityTest.java | 14 +- ...onExistingNotFoundLazyBatchEntityTest.java | 217 ++++++++++++++++++ ...tOverrideAsPersistentMappedSuperclass.java | 5 + ...nsientOverrideAsPersistentSingleTable.java | 5 + ...ientOverrideAsPersistentTablePerClass.java | 5 + .../test/notfound/EagerProxyNotFoundTest.java | 34 +++ .../orm/test/notfound/FkRefTests.java | 24 +- .../notfound/NotFoundAndSelectJoinTest.java | 3 +- .../NotFoundExceptionLogicalOneToOneTest.java | 4 +- .../sql/NativeQueryEagerAssociationTest.java | 27 ++- 15 files changed, 341 insertions(+), 66 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundLazyBatchEntityTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index e3996f66e6..057a00b3f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -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, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityJoinedFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityJoinedFetchInitializer.java index 6b2a30fb7d..109e5a854c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityJoinedFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityJoinedFetchInitializer.java @@ -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 diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java index 410095e95b..7fadc048ae 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/BatchFetchNotFoundIgnoreDynamicStyleTest.java @@ -115,9 +115,8 @@ public class BatchFetchNotFoundIgnoreDynamicStyleTest { final List 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 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() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java index 466b8ec5a4..16cc478845 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/notfound/LazyNotFoundOneToOneTest.java @@ -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(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java index dedd85049b..eaed1580d4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundBatchEntityTest.java @@ -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; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java index 85c157a7af..7e38757e0d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundEntityTest.java @@ -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 diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundLazyBatchEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundLazyBatchEntityTest.java new file mode 100644 index 0000000000..cc27b00bc0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/LoadANonExistingNotFoundLazyBatchEntityTest.java @@ -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 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; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentMappedSuperclass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentMappedSuperclass.java index b28e086c40..60aa50f9f1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentMappedSuperclass.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentMappedSuperclass.java @@ -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; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentSingleTable.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentSingleTable.java index e01a9d3fdf..a09b64d715 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentSingleTable.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentSingleTable.java @@ -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; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentTablePerClass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentTablePerClass.java index f897ddc93b..ba8ea3e813 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentTablePerClass.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/TransientOverrideAsPersistentTablePerClass.java @@ -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; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/EagerProxyNotFoundTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/EagerProxyNotFoundTest.java index 5f2c74449e..30ebb63070 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/EagerProxyNotFoundTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/EagerProxyNotFoundTest.java @@ -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( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/FkRefTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/FkRefTests.java index 1ba2fb0f24..b51259d6c6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/FkRefTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/FkRefTests.java @@ -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(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/NotFoundAndSelectJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/NotFoundAndSelectJoinTest.java index 7705cd45ae..370599911a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/NotFoundAndSelectJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/NotFoundAndSelectJoinTest.java @@ -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 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java index 78f622597e..9746b0bf46 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionLogicalOneToOneTest.java @@ -224,7 +224,7 @@ public class NotFoundExceptionLogicalOneToOneTest { final List 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; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryEagerAssociationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryEagerAssociationTest.java index d4d87d7032..34ecb2b3d9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryEagerAssociationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryEagerAssociationTest.java @@ -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)