From ac5a708ffaeb94dbeede40bbfeb0ec30ebb5df00 Mon Sep 17 00:00:00 2001 From: Thomas Alken Date: Tue, 21 Jan 2025 10:21:57 +0100 Subject: [PATCH 1/4] Bugfix for Issue 3622: ISession.Refresh does not update lazy properties --- .../FetchLazyPropertiesFixture.cs | 35 +++++++++++++++++++ src/NHibernate/Engine/EntityEntry.cs | 4 +-- src/NHibernate/Engine/IPersistenceContext.cs | 2 -- src/NHibernate/Engine/TwoPhaseLoad.cs | 2 -- src/NHibernate/Loader/Loader.cs | 11 +++--- .../Tuple/Entity/PocoEntityTuplizer.cs | 9 ++++- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 1fd3b8ed063..0f7ac568717 100644 --- a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1076,6 +1076,41 @@ void AssertPersons(List results, bool fetched) } } + [Test] + public void TestRefreshRemovesLazyLoadedProperties() + { + using (var outerSession = OpenSession()) + { + const string query = "from Person fetch Image where Id = 1"; + const string namePostFix = "_MODIFIED"; + const int imageLength = 4711; + + Person outerPerson = outerSession.CreateQuery(query).UniqueResult(); + + Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.False); // Normal property + Assert.That(outerPerson.Image.Length, Is.EqualTo(1)); // Lazy Property + + // Changing the properties of the person in a different sessions + using (var innerSession = OpenSession()) + { + var transaction = innerSession.BeginTransaction(); + + Person innerPerson = innerSession.CreateQuery(query).UniqueResult(); + innerPerson.Image = new byte[imageLength]; + innerPerson.Name += namePostFix; + innerSession.Update(innerPerson); + + transaction.Commit(); + } + + // Refreshing the person in the outer session + outerSession.Refresh(outerPerson); + + Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.True); // Value has changed + Assert.That(outerPerson.Image.Length, Is.EqualTo(imageLength)); // This is still the old value + } + } + private static Person GeneratePerson(int i, Person bestFriend) { return new Person diff --git a/src/NHibernate/Engine/EntityEntry.cs b/src/NHibernate/Engine/EntityEntry.cs index ac702632adb..70a473f7297 100644 --- a/src/NHibernate/Engine/EntityEntry.cs +++ b/src/NHibernate/Engine/EntityEntry.cs @@ -200,9 +200,7 @@ public object RowId { get { return rowId; } } - - // Since 5.3 - [Obsolete("This property is not used and will be removed in a future version.")] + public bool LoadedWithLazyPropertiesUnfetched { get { return loadedWithLazyPropertiesUnfetched; } diff --git a/src/NHibernate/Engine/IPersistenceContext.cs b/src/NHibernate/Engine/IPersistenceContext.cs index f23f9731788..b8f4a3d65b6 100644 --- a/src/NHibernate/Engine/IPersistenceContext.cs +++ b/src/NHibernate/Engine/IPersistenceContext.cs @@ -173,8 +173,6 @@ EntityEntry AddEntity(object entity, Status status, object[] loadedState, Entity /// Generates an appropriate EntityEntry instance and adds it /// to the event source's internal caches. /// - // Since 5.3 - [Obsolete("Use the AddEntry extension method instead")] EntityEntry AddEntry(object entity, Status status, object[] loadedState, object rowId, object id, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched); diff --git a/src/NHibernate/Engine/TwoPhaseLoad.cs b/src/NHibernate/Engine/TwoPhaseLoad.cs index 8ba9038b620..f304bd19b76 100644 --- a/src/NHibernate/Engine/TwoPhaseLoad.cs +++ b/src/NHibernate/Engine/TwoPhaseLoad.cs @@ -30,8 +30,6 @@ public static partial class TwoPhaseLoad /// to resolve any associations yet, because there might be other entities waiting to be /// read from the JDBC result set we are currently processing /// - // Since 5.3 - [Obsolete("Use the overload without lazyPropertiesAreUnfetched parameter instead")] public static void PostHydrate(IEntityPersister persister, object id, object[] values, object rowId, object obj, LockMode lockMode, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { object version = Versioning.GetVersion(values, persister); diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index ad3b5109021..d795a9dd9e4 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1147,7 +1147,8 @@ private object InstanceNotYetLoaded(DbDataReader dr, int i, ILoadable persister, ILoadable concretePersister = GetConcretePersister(dr, i, persister, key.Identifier, session); - if (optionalObjectKey != null && key.Equals(optionalObjectKey)) + bool useOptionalObject = optionalObjectKey != null && key.Equals(optionalObjectKey); + if (useOptionalObject) { // its the given optional object obj = optionalObject; @@ -1163,8 +1164,8 @@ private object InstanceNotYetLoaded(DbDataReader dr, int i, ILoadable persister, // (but don't yet initialize the object itself) // note that we acquired LockMode.READ even if it was not requested LockMode acquiredLockMode = lockMode == LockMode.None ? LockMode.Read : lockMode; - LoadFromResultSet(dr, i, obj, concretePersister, key, acquiredLockMode, persister, session); - + LoadFromResultSet(dr, i, obj, concretePersister, key, acquiredLockMode, persister, session, useOptionalObject); + // materialize associations (and initialize the object) later hydratedObjects.Add(obj); @@ -1291,7 +1292,7 @@ internal static void UpdateCacheForEntity( /// private void LoadFromResultSet(DbDataReader rs, int i, object obj, ILoadable persister, EntityKey key, LockMode lockMode, ILoadable rootPersister, - ISessionImplementor session) + ISessionImplementor session, bool lazyPropertiesAreUnfetched) { object id = key.Identifier; @@ -1316,7 +1317,7 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, ILoadable per object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; - TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, session); + TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, lazyPropertiesAreUnfetched, session); } private string[][] GetSubclassEntityAliases(int i, ILoadable persister) diff --git a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs index 2a79d56ca74..798655936c9 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs @@ -226,7 +226,7 @@ public override void AfterInitialize(object entity, ISessionImplementor session) if (IsInstrumented) { var interceptor = _enhancementMetadata.ExtractInterceptor(entity); - if (interceptor == null) + if (interceptor == null || HasAnyInitializedLazyProperty(entity, session)) { interceptor = _enhancementMetadata.InjectInterceptor(entity, session); } @@ -239,6 +239,13 @@ public override void AfterInitialize(object entity, ISessionImplementor session) } } + private static bool HasAnyInitializedLazyProperty(object entity, ISessionImplementor session) + { + IPersistenceContext persistenceContext = session.PersistenceContext; + EntityEntry entityEntry = persistenceContext.GetEntry(entity); + return entityEntry.LoadedWithLazyPropertiesUnfetched; + } + public override object GetPropertyValue(object entity, int i) { if (isBytecodeProviderImpl && optimizer?.AccessOptimizer != null) From 1aaa0d07882e596d5214a0ef797563dd2fccce7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 09:27:29 +0000 Subject: [PATCH 2/4] Generate async files --- .../FetchLazyPropertiesFixture.cs | 35 +++++++++++++++++++ src/NHibernate/Async/Loader/Loader.cs | 11 +++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs index aeb9eb71a9d..6f712ebe704 100644 --- a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1087,6 +1087,41 @@ void AssertPersons(List results, bool fetched) } } + [Test] + public async Task TestRefreshRemovesLazyLoadedPropertiesAsync() + { + using (var outerSession = OpenSession()) + { + const string query = "from Person fetch Image where Id = 1"; + const string namePostFix = "_MODIFIED"; + const int imageLength = 4711; + + Person outerPerson = await (outerSession.CreateQuery(query).UniqueResultAsync()); + + Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.False); // Normal property + Assert.That(outerPerson.Image.Length, Is.EqualTo(1)); // Lazy Property + + // Changing the properties of the person in a different sessions + using (var innerSession = OpenSession()) + { + var transaction = innerSession.BeginTransaction(); + + Person innerPerson = await (innerSession.CreateQuery(query).UniqueResultAsync()); + innerPerson.Image = new byte[imageLength]; + innerPerson.Name += namePostFix; + await (innerSession.UpdateAsync(innerPerson)); + + await (transaction.CommitAsync()); + } + + // Refreshing the person in the outer session + await (outerSession.RefreshAsync(outerPerson)); + + Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.True); // Value has changed + Assert.That(outerPerson.Image.Length, Is.EqualTo(imageLength)); // This is still the old value + } + } + private static Person GeneratePerson(int i, Person bestFriend) { return new Person diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index f797434cbe4..94437d36cec 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -757,7 +757,8 @@ private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo ILoadable concretePersister = await (GetConcretePersisterAsync(dr, i, persister, key.Identifier, session, cancellationToken)).ConfigureAwait(false); - if (optionalObjectKey != null && key.Equals(optionalObjectKey)) + bool useOptionalObject = optionalObjectKey != null && key.Equals(optionalObjectKey); + if (useOptionalObject) { // its the given optional object obj = optionalObject; @@ -773,8 +774,8 @@ private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo // (but don't yet initialize the object itself) // note that we acquired LockMode.READ even if it was not requested LockMode acquiredLockMode = lockMode == LockMode.None ? LockMode.Read : lockMode; - await (LoadFromResultSetAsync(dr, i, obj, concretePersister, key, acquiredLockMode, persister, session, cancellationToken)).ConfigureAwait(false); - + await (LoadFromResultSetAsync(dr, i, obj, concretePersister, key, acquiredLockMode, persister, session, useOptionalObject, cancellationToken)).ConfigureAwait(false); + // materialize associations (and initialize the object) later hydratedObjects.Add(obj); @@ -886,7 +887,7 @@ internal static async Task UpdateCacheForEntityAsync( /// private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, ILoadable persister, EntityKey key, LockMode lockMode, ILoadable rootPersister, - ISessionImplementor session, CancellationToken cancellationToken) + ISessionImplementor session, bool lazyPropertiesAreUnfetched, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); object id = key.Identifier; @@ -912,7 +913,7 @@ private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, IL object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; - TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, session); + TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, lazyPropertiesAreUnfetched, session); } /// From d64a25cf58e7f1e5f40fc5a363ffa84dc9ba4bc6 Mon Sep 17 00:00:00 2001 From: Thomas Alken Date: Wed, 22 Jan 2025 09:01:42 +0100 Subject: [PATCH 3/4] Changed imageLength to 1000 to respect oracle restriction --- .../FetchLazyProperties/FetchLazyPropertiesFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 0f7ac568717..93528c24bd3 100644 --- a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1083,7 +1083,7 @@ public void TestRefreshRemovesLazyLoadedProperties() { const string query = "from Person fetch Image where Id = 1"; const string namePostFix = "_MODIFIED"; - const int imageLength = 4711; + const int imageLength = 1000; Person outerPerson = outerSession.CreateQuery(query).UniqueResult(); From 68ac4099a426567877111bf8c59fe23fd87a34bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jan 2025 08:04:04 +0000 Subject: [PATCH 4/4] Generate async files --- .../Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 6f712ebe704..814d8236b38 100644 --- a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1094,7 +1094,7 @@ public async Task TestRefreshRemovesLazyLoadedPropertiesAsync() { const string query = "from Person fetch Image where Id = 1"; const string namePostFix = "_MODIFIED"; - const int imageLength = 4711; + const int imageLength = 1000; Person outerPerson = await (outerSession.CreateQuery(query).UniqueResultAsync());