Skip to content

Commit 1d77131

Browse files
committed
Added the ability to update lazy properties of already loaded instances to fix the issue when fetching lazy properties on a self many to one relation
1 parent 192f301 commit 1d77131

File tree

10 files changed

+385
-25
lines changed

10 files changed

+385
-25
lines changed

src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ protected override void OnTearDown()
6565
using (var tx = s.BeginTransaction())
6666
{
6767
s.CreateQuery("delete from Animal").ExecuteUpdate();
68+
s.CreateQuery("update Person set BestFriend = null").ExecuteUpdate();
6869
s.CreateQuery("delete from Person").ExecuteUpdate();
6970
tx.Commit();
7071
}
@@ -266,15 +267,78 @@ public async Task TestHqlFetchFormulaAndManyToOneComponentAsync()
266267
Person person;
267268
using (var s = OpenSession())
268269
{
269-
person = (await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address")
270-
.ListAsync<Person>())).FirstOrDefault(o => o.Id == 1);
270+
person = await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address where p.Id = 1")
271+
.UniqueResultAsync<Person>());
271272
}
272273

273274
await (AssertFetchFormulaAndManyToOneComponentAsync(person));
274275
}
275276

276277
[Test]
277278
public async Task TestLinqFetchFormulaAndManyToOneComponentAsync()
279+
{
280+
Person person;
281+
using (var s = OpenSession())
282+
{
283+
person = await (s.Query<Person>()
284+
.Fetch(o => o.Formula)
285+
.Fetch(o => o.BestFriend).ThenFetch(o => o.Address)
286+
.FirstOrDefaultAsync(o => o.Id == 1));
287+
288+
}
289+
290+
await (AssertFetchFormulaAndManyToOneComponentAsync(person));
291+
}
292+
293+
private static Task AssertFetchFormulaAndManyToOneComponentAsync(Person person)
294+
{
295+
try
296+
{
297+
Assert.That(person, Is.Not.Null);
298+
299+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
300+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
301+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True);
302+
// Currently v5.2 NHibernateUtil.IsPropertyInitialized will always return true for associations
303+
// when the class have lazy properties
304+
Assert.DoesNotThrow(() =>
305+
{
306+
var name = person.BestFriend.Name;
307+
});
308+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False);
309+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True);
310+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False);
311+
312+
Assert.That(person.Formula, Is.EqualTo(1));
313+
Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2"));
314+
Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2"));
315+
return Task.CompletedTask;
316+
}
317+
catch (Exception ex)
318+
{
319+
return Task.FromException<object>(ex);
320+
}
321+
}
322+
323+
#endregion
324+
325+
#region FetchFormulaAndManyToOneComponentList
326+
327+
[Test]
328+
public async Task TestHqlFetchFormulaAndManyToOneComponentListAsync()
329+
{
330+
Person person;
331+
using (var s = OpenSession())
332+
{
333+
person = (await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address")
334+
.ListAsync<Person>())).FirstOrDefault(o => o.Id == 1);
335+
}
336+
337+
await (AssertFetchFormulaAndManyToOneComponentListAsync(person));
338+
}
339+
340+
[Test]
341+
public async Task TestLinqFetchFormulaAndManyToOneComponentListAsync()
278342
{
279343
Person person;
280344
using (var s = OpenSession())
@@ -287,10 +351,10 @@ public async Task TestLinqFetchFormulaAndManyToOneComponentAsync()
287351

288352
}
289353

290-
await (AssertFetchFormulaAndManyToOneComponentAsync(person));
354+
await (AssertFetchFormulaAndManyToOneComponentListAsync(person));
291355
}
292356

293-
private static Task AssertFetchFormulaAndManyToOneComponentAsync(Person person)
357+
private static Task AssertFetchFormulaAndManyToOneComponentListAsync(Person person)
294358
{
295359
try
296360
{
@@ -307,7 +371,7 @@ private static Task AssertFetchFormulaAndManyToOneComponentAsync(Person person)
307371
});
308372
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False);
309373
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True);
310-
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False);
374+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True);
311375

312376
Assert.That(person.Formula, Is.EqualTo(1));
313377
Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2"));

src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ protected override void OnTearDown()
5454
using (var tx = s.BeginTransaction())
5555
{
5656
s.CreateQuery("delete from Animal").ExecuteUpdate();
57+
s.CreateQuery("update Person set BestFriend = null").ExecuteUpdate();
5758
s.CreateQuery("delete from Person").ExecuteUpdate();
5859
tx.Commit();
5960
}
@@ -255,15 +256,70 @@ public void TestHqlFetchFormulaAndManyToOneComponent()
255256
Person person;
256257
using (var s = OpenSession())
257258
{
258-
person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address")
259-
.List<Person>().FirstOrDefault(o => o.Id == 1);
259+
person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address where p.Id = 1")
260+
.UniqueResult<Person>();
260261
}
261262

262263
AssertFetchFormulaAndManyToOneComponent(person);
263264
}
264265

265266
[Test]
266267
public void TestLinqFetchFormulaAndManyToOneComponent()
268+
{
269+
Person person;
270+
using (var s = OpenSession())
271+
{
272+
person = s.Query<Person>()
273+
.Fetch(o => o.Formula)
274+
.Fetch(o => o.BestFriend).ThenFetch(o => o.Address)
275+
.FirstOrDefault(o => o.Id == 1);
276+
277+
}
278+
279+
AssertFetchFormulaAndManyToOneComponent(person);
280+
}
281+
282+
private static void AssertFetchFormulaAndManyToOneComponent(Person person)
283+
{
284+
Assert.That(person, Is.Not.Null);
285+
286+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
287+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
288+
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True);
289+
// Currently v5.2 NHibernateUtil.IsPropertyInitialized will always return true for associations
290+
// when the class have lazy properties
291+
Assert.DoesNotThrow(() =>
292+
{
293+
var name = person.BestFriend.Name;
294+
});
295+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False);
296+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True);
297+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False);
298+
299+
Assert.That(person.Formula, Is.EqualTo(1));
300+
Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2"));
301+
Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2"));
302+
}
303+
304+
#endregion
305+
306+
#region FetchFormulaAndManyToOneComponentList
307+
308+
[Test]
309+
public void TestHqlFetchFormulaAndManyToOneComponentList()
310+
{
311+
Person person;
312+
using (var s = OpenSession())
313+
{
314+
person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address")
315+
.List<Person>().FirstOrDefault(o => o.Id == 1);
316+
}
317+
318+
AssertFetchFormulaAndManyToOneComponentList(person);
319+
}
320+
321+
[Test]
322+
public void TestLinqFetchFormulaAndManyToOneComponentList()
267323
{
268324
Person person;
269325
using (var s = OpenSession())
@@ -276,10 +332,10 @@ public void TestLinqFetchFormulaAndManyToOneComponent()
276332

277333
}
278334

279-
AssertFetchFormulaAndManyToOneComponent(person);
335+
AssertFetchFormulaAndManyToOneComponentList(person);
280336
}
281337

282-
private static void AssertFetchFormulaAndManyToOneComponent(Person person)
338+
private static void AssertFetchFormulaAndManyToOneComponentList(Person person)
283339
{
284340
Assert.That(person, Is.Not.Null);
285341

@@ -294,7 +350,7 @@ private static void AssertFetchFormulaAndManyToOneComponent(Person person)
294350
});
295351
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False);
296352
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True);
297-
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False);
353+
Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True);
298354

299355
Assert.That(person.Formula, Is.EqualTo(1));
300356
Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2"));

src/NHibernate.Test/FetchLazyProperties/Mappings.hbm.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
</id>
99
<property name="Name" />
1010
<property name="Image" lazy="true" />
11-
<property name="Formula" formula="(select Id)" lazy="true" />
11+
<property name="Formula" formula="(Id)" lazy="true" />
1212
<component name="Address" lazy="true">
1313
<property name="Country" />
1414
<property name="City" />
1515
</component>
16-
<many-to-one name="BestFriend" column="BestFriendId" class="Person" cascade="save-update" />
16+
<many-to-one name="BestFriend" column="BestFriendId" class="Person" cascade="all" />
1717
<set name="Pets" cascade="all-delete-orphan" inverse="true">
1818
<key column="OwnerId" not-null="true" />
1919
<one-to-many class="Animal" />
@@ -35,14 +35,14 @@
3535
<property name="Name" />
3636
<many-to-one name="Owner" column="OwnerId" class="Person" />
3737
<property name="Image" lazy="true" />
38-
<property name="Formula" formula="(select Id)" lazy="true" />
38+
<property name="Formula" formula="(Id)" lazy="true" />
3939
<component name="Address" lazy="true">
4040
<property name="Country" />
4141
<property name="City" />
4242
</component>
4343
<union-subclass name="Cat">
4444
<property name="SecondImage" lazy="true" />
45-
<property name="SecondFormula" formula="(select Name)" lazy="true" />
45+
<property name="SecondFormula" formula="(Name)" lazy="true" />
4646
</union-subclass>
4747
<union-subclass name="Dog">
4848
</union-subclass>

src/NHibernate/Async/Loader/Loader.cs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ private async Task<object[]> GetRowAsync(DbDataReader rs, ILoadable[] persisters
697697
/// <summary>
698698
/// The entity instance is already in the session cache
699699
/// </summary>
700-
private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPersister persister, EntityKey key, object obj,
700+
private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, ILoadable persister, EntityKey key, object obj,
701701
LockMode lockMode, ISessionImplementor session, CancellationToken cancellationToken)
702702
{
703703
cancellationToken.ThrowIfCancellationRequested();
@@ -707,9 +707,10 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer
707707
throw new WrongClassException(errorMsg, key.Identifier, persister.EntityName);
708708
}
709709

710+
EntityEntry entry = null;
710711
if (LockMode.None != lockMode && UpgradeLocks())
711712
{
712-
EntityEntry entry = session.PersistenceContext.GetEntry(obj);
713+
entry = session.PersistenceContext.GetEntry(obj);
713714
bool isVersionCheckNeeded = persister.IsVersioned && entry.LockMode.LessThan(lockMode);
714715

715716
// we don't need to worry about existing version being uninitialized
@@ -723,6 +724,45 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer
723724
entry.LockMode = lockMode;
724725
}
725726
}
727+
728+
if (!persister.HasLazyProperties)
729+
{
730+
return;
731+
}
732+
733+
entry = entry ?? session.PersistenceContext.GetEntry(obj);
734+
if (!entry.LoadedWithLazyPropertiesUnfetched)
735+
{
736+
return; // All lazy properties were already loaded
737+
}
738+
739+
var eagerPropertyFetch = IsEagerPropertyFetchEnabled(i);
740+
var eagerFetchProperties = GetEagerFetchProperties(i);
741+
742+
if (!eagerPropertyFetch && eagerFetchProperties == null)
743+
{
744+
return; // No lazy properties were loaded
745+
}
746+
747+
var updateLazyProperties = eagerFetchProperties?.Except(entry.FetchedLazyProperties ?? Enumerable.Empty<string>()).ToArray();
748+
if (updateLazyProperties?.Length == 0)
749+
{
750+
return; // No new lazy properites were loaded
751+
}
752+
753+
var instanceClass = await (GetInstanceClassAsync(rs, i, persister, key.Identifier, session, cancellationToken)).ConfigureAwait(false);
754+
await (UpdateLazyPropertiesFromResultSetAsync(rs, i, obj, instanceClass, key, updateLazyProperties, eagerPropertyFetch,
755+
entry.LoadedState, persister, session, cancellationToken)).ConfigureAwait(false);
756+
757+
if (eagerPropertyFetch)
758+
{
759+
entry.LoadedWithLazyPropertiesUnfetched = false;
760+
entry.FetchedLazyProperties = null;
761+
}
762+
else
763+
{
764+
entry.FetchedLazyProperties = eagerFetchProperties.Union(entry.FetchedLazyProperties ?? Enumerable.Empty<string>()).ToArray();
765+
}
726766
}
727767

728768
/// <summary>
@@ -761,6 +801,40 @@ private async Task<object> InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo
761801
return obj;
762802
}
763803

804+
private Task UpdateLazyPropertiesFromResultSetAsync(DbDataReader rs, int i, object obj, string instanceClass, EntityKey key,
805+
string[] updateLazyProperties, bool updateAllLazyProperties, object[] loadedState,
806+
ILoadable rootPersister, ISessionImplementor session, CancellationToken cancellationToken)
807+
{
808+
if (cancellationToken.IsCancellationRequested)
809+
{
810+
return Task.FromCanceled<object>(cancellationToken);
811+
}
812+
try
813+
{
814+
var id = key.Identifier;
815+
816+
// Get the persister for the _subclass_
817+
var persister = instanceClass == rootPersister.EntityName
818+
? rootPersister
819+
: (ILoadable) Factory.GetEntityPersister(instanceClass);
820+
821+
if (Log.IsDebugEnabled())
822+
{
823+
Log.Debug("Updating lazy properites from DataReader: {0}", MessageHelper.InfoString(persister, id));
824+
}
825+
826+
var cols = persister == rootPersister
827+
? EntityAliases[i].SuffixedPropertyAliases
828+
: GetSubclassEntityAliases(i, persister);
829+
830+
return persister.HydrateLazyPropertiesAsync(rs, id, obj, rootPersister, cols, updateLazyProperties, updateAllLazyProperties, loadedState, session, cancellationToken);
831+
}
832+
catch (Exception ex)
833+
{
834+
return Task.FromException<object>(ex);
835+
}
836+
}
837+
764838
/// <summary>
765839
/// Hydrate the state of an object from the SQL <c>DbDataReader</c>, into
766840
/// an array of "hydrated" values (do not resolve associations yet),

0 commit comments

Comments
 (0)