diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java
index 2e185d1e15..fa74063e7b 100644
--- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java
+++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java
@@ -754,6 +754,7 @@ public class MappingModelCreationHelper {
final FetchTiming timing = FetchOptionsHelper.determineFetchTiming(
style,
collectionDescriptor.getCollectionType(),
+ collectionDescriptor.isLazy(),
sessionFactory
);
@@ -1583,23 +1584,22 @@ public class MappingModelCreationHelper {
final boolean lazy = value.isLazy();
if ( lazy && entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ) {
if ( value.isUnwrapProxy() ) {
- fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, sessionFactory );
+ fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, lazy, sessionFactory );
}
else if ( value instanceof ManyToOne && value.isNullable() && ( (ManyToOne) value ).isIgnoreNotFound() ) {
fetchTiming = FetchTiming.IMMEDIATE;
}
else {
- fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, sessionFactory );
+ fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, lazy, sessionFactory );
}
}
- else if ( fetchStyle == FetchStyle.JOIN
- || !lazy
+ else if ( !lazy
|| value instanceof OneToOne && value.isNullable()
|| value instanceof ManyToOne && value.isNullable() && ( (ManyToOne) value ).isIgnoreNotFound() ) {
fetchTiming = FetchTiming.IMMEDIATE;
}
else {
- fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, sessionFactory );
+ fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, lazy, sessionFactory );
}
final ToOneAttributeMapping attributeMapping = new ToOneAttributeMapping(
diff --git a/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchOptionsHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchOptionsHelper.java
index 23b89bd3d1..9544800ce7 100644
--- a/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchOptionsHelper.java
+++ b/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchOptionsHelper.java
@@ -126,6 +126,25 @@ public final class FetchOptionsHelper {
}
}
+ public static FetchTiming determineFetchTiming(
+ FetchStyle style,
+ AssociationType type,
+ boolean lazy,
+ SessionFactoryImplementor sessionFactory) {
+ switch ( style ) {
+ case JOIN: {
+ return lazy ? FetchTiming.DELAYED : FetchTiming.IMMEDIATE;
+ }
+ case BATCH:
+ case SUBSELECT:
+ default: {
+ return isSubsequentSelectDelayed( type, sessionFactory )
+ ? FetchTiming.DELAYED
+ : FetchTiming.IMMEDIATE;
+ }
+ }
+ }
+
private static boolean isSubsequentSelectDelayed(AssociationType type, SessionFactoryImplementor sessionFactory) {
if ( type.isAnyType() ) {
// we'd need more context here. this is only kept as part of the property state on the owning entity
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/AbstractEmbeddableInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/AbstractEmbeddableInitializer.java
index 7f0985e5ed..5811b50c50 100644
--- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/AbstractEmbeddableInitializer.java
+++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/AbstractEmbeddableInitializer.java
@@ -254,6 +254,8 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
private void extractRowState(RowProcessingState processingState) {
stateAllNull = true;
+ final boolean isKey = ForeignKeyDescriptor.PART_NAME.equals( navigablePath.getLocalName() )
+ || EntityIdentifierMapping.ROLE_LOCAL_NAME.equals( embedded.getFetchableName() );
for ( int i = 0; i < assemblers.size(); i++ ) {
final DomainResultAssembler> assembler = assemblers.get( i );
final Object contributorValue = assembler.assemble(
@@ -265,6 +267,11 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
if ( contributorValue != null ) {
stateAllNull = false;
}
+ else if ( isKey ) {
+ // If this is a foreign key and there is a null part, the whole thing has to be turned into null
+ stateAllNull = true;
+ break;
+ }
}
applyMapsId( processingState );
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/formula/JoinFormulaManyToOneLazyFetchingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/formula/JoinFormulaManyToOneLazyFetchingTest.java
index 69244de631..5684cdf31d 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/formula/JoinFormulaManyToOneLazyFetchingTest.java
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/formula/JoinFormulaManyToOneLazyFetchingTest.java
@@ -91,15 +91,9 @@ public class JoinFormulaManyToOneLazyFetchingTest extends BaseEntityManagerFunct
assertEquals( 2, stocks.size() );
assertEquals( "ABC", stocks.get( 0 ).getCode().getRefNumber() );
- try {
- stocks.get( 1 ).getCode().getRefNumber();
-
- fail( "Should have thrown EntityNotFoundException" );
- }
- catch (EntityNotFoundException expected) {
-
- }
-
+ // In 5.x, for some reason, we didn't understand that a component is a FK,
+ // hence a partial null was wrongly handled, but in 6.0 we handle this in a unified way
+ assertNull( stocks.get( 1 ).getCode() );
} );
}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/backref/map/compkey/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/backref/map/compkey/Mappings.hbm.xml
index 3c7a60ea70..d5213464e6 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/backref/map/compkey/Mappings.hbm.xml
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/backref/map/compkey/Mappings.hbm.xml
@@ -19,7 +19,7 @@
-
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/basic/UserPermissions.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/basic/UserPermissions.hbm.xml
index d225834939..4e75a543e2 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/basic/UserPermissions.hbm.xml
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/basic/UserPermissions.hbm.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/declaredtype/UserPermissions.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/declaredtype/UserPermissions.hbm.xml
index c9649d9c75..33cf51c0a9 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/declaredtype/UserPermissions.hbm.xml
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/declaredtype/UserPermissions.hbm.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/parameterized/Mapping.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/parameterized/Mapping.hbm.xml
index 3e710c3f0d..f9deb780e0 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/parameterized/Mapping.hbm.xml
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/custom/parameterized/Mapping.hbm.xml
@@ -17,7 +17,7 @@
-
+
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/typedmanytoone/TypedManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/typedmanytoone/TypedManyToOneTest.java
index 3eb48a56bb..db14a67b84 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/typedmanytoone/TypedManyToOneTest.java
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/typedmanytoone/TypedManyToOneTest.java
@@ -28,10 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@SessionFactory
public class TypedManyToOneTest {
@Test
- @NotImplementedYet(
- strict = false,
- reason = "fetch=\"join\" in `hbm.xml` mappings for collection (lazy v. eager)"
- )
public void testCreateQuery(SessionFactoryScope scope) {
final Customer cust = new Customer();
cust.setCustomerId("abc123");
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/orphan/Product.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/orphan/Product.hbm.xml
index f6ad16a69d..e761007f34 100644
--- a/hibernate-core/src/test/java/org/hibernate/orm/test/orphan/Product.hbm.xml
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/orphan/Product.hbm.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/migration-guide.adoc b/migration-guide.adoc
index e7c02c9ab6..5f6bab0629 100644
--- a/migration-guide.adoc
+++ b/migration-guide.adoc
@@ -700,3 +700,10 @@ The SQL generation is now fully handled through the `SqlAstTranslator` which a `
Legacy `hbm.xml` mapping format is considered deprecated and will no longer supported beyond 6.x.
+== Association laziness now respected
+
+Prior to Hibernate 6.0, lazy associations that used `fetch="join"` or `@Fetch(FetchMode.JOIN)` were considered eager
+when loaded by-id i.e. through `Session#get`/`EntityManager#find`, even though for queries the association was treated as lazy.
+
+Starting with Hibernate 6.0, the laziness of such associations is properly respected, regardless of the fetch mechanism.
+Backwards compatibility can be achieved by specifying `lazy="false"` or `@ManyToOne(fetch = EAGER)`/`@OneToOne(fetch = EAGER)`/`@OneToMany(fetch = EAGER)`/`@ManyToMany(fetch = EAGER)`.
\ No newline at end of file