Skip to content

Commit a8f4ea3

Browse files
bahusoidhazzik
authored andcommitted
Fix future queries with non lazy associations (#2174)
1 parent e0b60cf commit a8f4ea3

File tree

13 files changed

+217
-25
lines changed

13 files changed

+217
-25
lines changed

src/AsyncGenerator.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
applyChanges: true
66
analyzation:
77
methodConversion:
8+
#TODO 6.0: Remove ignore rule for IQueryBatchItem.ProcessResults
9+
- conversion: Ignore
10+
name: ProcessResults
11+
containingTypeName: IQueryBatchItem
812
- conversion: Ignore
913
name: PostProcessInsert
1014
containingTypeName: HqlSqlWalker

src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,35 @@ public async Task CacheModeWorksWithFutureAsync()
498498
}
499499
}
500500

501+
//GH-2173
502+
[Test]
503+
public async Task CanFetchNonLazyEntitiesInSubsequentQueryAsync()
504+
{
505+
Sfi.Statistics.IsStatisticsEnabled = true;
506+
using (var s = OpenSession())
507+
using (var t = s.BeginTransaction())
508+
{
509+
await (s.SaveAsync(
510+
new EntityEager
511+
{
512+
Name = "EagerManyToOneAssociation",
513+
EagerEntity = new EntityEagerChild {Name = "association"}
514+
}));
515+
await (t.CommitAsync());
516+
}
517+
518+
using (var s = OpenSession())
519+
{
520+
Sfi.Statistics.Clear();
521+
//EntityEager.EagerEntity is lazy initialized instead of being loaded by the second query
522+
s.QueryOver<EntityEager>().Fetch(SelectMode.Skip, x => x.EagerEntity).Future();
523+
await (s.QueryOver<EntityEager>().Fetch(SelectMode.Fetch, x => x.EagerEntity).Future().GetEnumerableAsync());
524+
525+
if(SupportsMultipleQueries)
526+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
527+
}
528+
}
529+
501530
#region Test Setup
502531

503532
protected override HbmMapping GetMappings()
@@ -543,6 +572,10 @@ protected override HbmMapping GetMappings()
543572
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
544573
rc.Property(x => x.Name);
545574

575+
rc.ManyToOne(x => x.EagerEntity, m =>
576+
{
577+
m.Cascade(Mapping.ByCode.Cascade.Persist);
578+
});
546579
rc.Bag(ep => ep.ChildrenListSubselect,
547580
m =>
548581
{
@@ -560,6 +593,14 @@ protected override HbmMapping GetMappings()
560593
},
561594
a => a.OneToMany());
562595
});
596+
mapper.Class<EntityEagerChild>(
597+
rc =>
598+
{
599+
rc.Lazy(false);
600+
601+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
602+
rc.Property(x => x.Name);
603+
});
563604
mapper.Class<EntitySubselectChild>(
564605
rc =>
565606
{

src/NHibernate.Test/Futures/Entities.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ public class EntitySubselectChild
3636
public virtual EntityEager Parent { get; set; }
3737
}
3838

39+
public class EntityEagerChild
40+
{
41+
public Guid Id { get; set; }
42+
public string Name { get; set; }
43+
}
44+
3945
public class EntityEager
4046
{
4147
public Guid Id { get; set; }
4248
public string Name { get; set; }
4349

50+
public EntityEagerChild EagerEntity { get; set; }
4451
public IList<EntitySubselectChild> ChildrenListSubselect { get; set; }
4552
public IList<EntitySimpleChild> ChildrenListEager { get; set; } //= new HashSet<EntitySimpleChild>();
4653
}

src/NHibernate.Test/Futures/QueryBatchFixture.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,35 @@ public void CacheModeWorksWithFuture()
486486
}
487487
}
488488

489+
//GH-2173
490+
[Test]
491+
public void CanFetchNonLazyEntitiesInSubsequentQuery()
492+
{
493+
Sfi.Statistics.IsStatisticsEnabled = true;
494+
using (var s = OpenSession())
495+
using (var t = s.BeginTransaction())
496+
{
497+
s.Save(
498+
new EntityEager
499+
{
500+
Name = "EagerManyToOneAssociation",
501+
EagerEntity = new EntityEagerChild {Name = "association"}
502+
});
503+
t.Commit();
504+
}
505+
506+
using (var s = OpenSession())
507+
{
508+
Sfi.Statistics.Clear();
509+
//EntityEager.EagerEntity is lazy initialized instead of being loaded by the second query
510+
s.QueryOver<EntityEager>().Fetch(SelectMode.Skip, x => x.EagerEntity).Future();
511+
s.QueryOver<EntityEager>().Fetch(SelectMode.Fetch, x => x.EagerEntity).Future().GetEnumerable();
512+
513+
if(SupportsMultipleQueries)
514+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
515+
}
516+
}
517+
489518
#region Test Setup
490519

491520
protected override HbmMapping GetMappings()
@@ -531,6 +560,10 @@ protected override HbmMapping GetMappings()
531560
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
532561
rc.Property(x => x.Name);
533562

563+
rc.ManyToOne(x => x.EagerEntity, m =>
564+
{
565+
m.Cascade(Mapping.ByCode.Cascade.Persist);
566+
});
534567
rc.Bag(ep => ep.ChildrenListSubselect,
535568
m =>
536569
{
@@ -548,6 +581,14 @@ protected override HbmMapping GetMappings()
548581
},
549582
a => a.OneToMany());
550583
});
584+
mapper.Class<EntityEagerChild>(
585+
rc =>
586+
{
587+
rc.Lazy(false);
588+
589+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
590+
rc.Property(x => x.Name);
591+
});
551592
mapper.Class<EntitySubselectChild>(
552593
rc =>
553594
{

src/NHibernate/Async/Multi/IQueryBatchItem.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ public partial interface IQueryBatchItem
3636
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
3737
Task ExecuteNonBatchedAsync(CancellationToken cancellationToken);
3838
}
39+
40+
internal partial interface IQueryBatchItemWithAsyncProcessResults
41+
{
42+
Task ProcessResultsAsync(CancellationToken cancellationToken);
43+
}
3944
}

src/NHibernate/Async/Multi/QueryBatch.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,37 +127,48 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken)
127127
}
128128

129129
var rowCount = 0;
130+
CacheBatcher cacheBatcher = null;
130131
try
131132
{
132133
if (resultSetsCommand.HasQueries)
133134
{
135+
cacheBatcher = new CacheBatcher(Session);
134136
using (var reader = await (resultSetsCommand.GetReaderAsync(Timeout, cancellationToken)).ConfigureAwait(false))
135137
{
136-
var cacheBatcher = new CacheBatcher(Session);
137138
foreach (var query in _queries)
138139
{
139140
if (query.CachingInformation != null)
140141
{
141-
foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable))
142+
foreach (var cachingInfo in query.CachingInformation)
142143
{
143144
cachingInfo.SetCacheBatcher(cacheBatcher);
144145
}
145146
}
146147

147148
rowCount += await (query.ProcessResultsSetAsync(reader, cancellationToken)).ConfigureAwait(false);
148149
}
149-
await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false);
150150
}
151151
}
152152

153-
// Query cacheable results must be cached untransformed: the put does not need to wait for
154-
// the ProcessResults.
155-
await (PutCacheableResultsAsync(cancellationToken)).ConfigureAwait(false);
156-
157153
foreach (var query in _queries)
158154
{
159-
query.ProcessResults();
155+
//TODO 6.0: Replace with query.ProcessResults();
156+
if (query is IQueryBatchItemWithAsyncProcessResults q)
157+
await (q.ProcessResultsAsync(cancellationToken)).ConfigureAwait(false);
158+
else
159+
query.ProcessResults();
160160
}
161+
162+
var executeBatchTask = cacheBatcher?.ExecuteBatchAsync(cancellationToken);
163+
164+
if (executeBatchTask != null)
165+
166+
{
167+
168+
await (executeBatchTask).ConfigureAwait(false);
169+
170+
}
171+
await (PutCacheableResultsAsync(cancellationToken)).ConfigureAwait(false);
161172
}
162173
catch (OperationCanceledException) { throw; }
163174
catch (Exception sqle)

src/NHibernate/Async/Multi/QueryBatchItemBase.cs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace NHibernate.Multi
2323
{
2424
using System.Threading.Tasks;
2525
using System.Threading;
26-
public abstract partial class QueryBatchItemBase<TResult> : IQueryBatchItem<TResult>
26+
public abstract partial class QueryBatchItemBase<TResult> : IQueryBatchItem<TResult>, IQueryBatchItemWithAsyncProcessResults
2727
{
2828

2929
/// <inheritdoc />
@@ -103,17 +103,46 @@ public async Task<int> ProcessResultsSetAsync(DbDataReader reader, CancellationT
103103

104104
queryInfo.Result = tmpResults;
105105
if (queryInfo.CanPutToCache)
106-
queryInfo.ResultToCache = tmpResults;
106+
queryInfo.ResultToCache = new List<object>(tmpResults);
107107

108108
await (reader.NextResultAsync(cancellationToken)).ConfigureAwait(false);
109109
}
110110

111-
await (InitializeEntitiesAndCollectionsAsync(reader, hydratedObjects, cancellationToken)).ConfigureAwait(false);
112-
111+
StopLoadingCollections(reader);
112+
_reader = reader;
113+
_hydratedObjects = hydratedObjects;
113114
return rowCount;
114115
}
115116
}
116117

118+
/// <inheritdoc cref="IQueryBatchItem.ProcessResults" />
119+
public async Task ProcessResultsAsync(CancellationToken cancellationToken)
120+
{
121+
cancellationToken.ThrowIfCancellationRequested();
122+
ThrowIfNotInitialized();
123+
124+
using (Session.SwitchCacheMode(_cacheMode))
125+
await (InitializeEntitiesAndCollectionsAsync(_reader, _hydratedObjects, cancellationToken)).ConfigureAwait(false);
126+
127+
for (var i = 0; i < _queryInfos.Count; i++)
128+
{
129+
var queryInfo = _queryInfos[i];
130+
if (_subselectResultKeys[i] != null)
131+
{
132+
queryInfo.Loader.CreateSubselects(_subselectResultKeys[i], queryInfo.Parameters, Session);
133+
}
134+
135+
if (queryInfo.IsCacheable)
136+
{
137+
// This transformation must not be applied to ResultToCache.
138+
queryInfo.Result =
139+
queryInfo.Loader.TransformCacheableResults(
140+
queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, queryInfo.Result);
141+
}
142+
}
143+
AfterLoadCallback?.Invoke(GetResults());
144+
}
145+
117146
/// <inheritdoc />
118147
public async Task ExecuteNonBatchedAsync(CancellationToken cancellationToken)
119148
{

src/NHibernate/Engine/Loading/CollectionLoadContext.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ public IPersistentCollection GetLoadingCollection(ICollectionPersister persister
124124
}
125125
else
126126
{
127+
if (loadingCollectionEntry.StopLoading)
128+
return null;
127129
if (loadingCollectionEntry.ResultSet == resultSet)
128130
{
129131
log.Debug("found loading collection bound to current result set processing; reading row");
@@ -398,5 +400,17 @@ public override string ToString()
398400
{
399401
return base.ToString() + "<rs=" + ResultSet + ">";
400402
}
403+
404+
internal void StopLoadingCollections(ICollectionPersister[] collectionPersisters)
405+
{
406+
foreach (var collectionKey in localLoadingCollectionKeys)
407+
{
408+
var loadingCollectionEntry = LoadContext.LocateLoadingCollectionEntry(collectionKey);
409+
if (loadingCollectionEntry != null && Array.IndexOf(collectionPersisters, loadingCollectionEntry.Persister) >= 0)
410+
{
411+
loadingCollectionEntry.StopLoading = true;
412+
}
413+
}
414+
}
401415
}
402416
}

src/NHibernate/Engine/Loading/LoadingCollectionEntry.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public IPersistentCollection Collection
4444
get { return collection; }
4545
}
4646

47+
public bool StopLoading { get; set; }
48+
4749
public override string ToString()
4850
{
4951
return GetType().FullName + "<rs=" + ResultSet + ", coll=" + MessageHelper.InfoString(Persister.Role, Key) + ">@" + Convert.ToString(GetHashCode(), 16);

src/NHibernate/Loader/Loader.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,18 @@ internal void InitializeEntitiesAndCollections(
672672
}
673673
}
674674

675+
/// <summary>
676+
/// Stops further collection population without actual collection initialization.
677+
/// </summary>
678+
internal void StopLoadingCollections(ISessionImplementor session, DbDataReader reader)
679+
{
680+
var collectionPersisters = CollectionPersisters;
681+
if (collectionPersisters == null || collectionPersisters.Length == 0)
682+
return;
683+
684+
session.PersistenceContext.LoadContexts.GetCollectionLoadContext(reader).StopLoadingCollections(collectionPersisters);
685+
}
686+
675687
private void EndCollectionLoad(DbDataReader reader, ISessionImplementor session, ICollectionPersister collectionPersister)
676688
{
677689
//this is a query and we are loading multiple instances of the same collection role

src/NHibernate/Multi/IQueryBatchItem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,10 @@ public partial interface IQueryBatchItem
8080
/// </summary>
8181
void ExecuteNonBatched();
8282
}
83+
84+
//TODO 6.0: Remove along with ignore rule for IQueryBatchItem.ProcessResults in AsyncGenerator.yml
85+
internal partial interface IQueryBatchItemWithAsyncProcessResults
86+
{
87+
void ProcessResults();
88+
}
8389
}

src/NHibernate/Multi/QueryBatch.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,37 +153,40 @@ protected void ExecuteBatched()
153153
}
154154

155155
var rowCount = 0;
156+
CacheBatcher cacheBatcher = null;
156157
try
157158
{
158159
if (resultSetsCommand.HasQueries)
159160
{
161+
cacheBatcher = new CacheBatcher(Session);
160162
using (var reader = resultSetsCommand.GetReader(Timeout))
161163
{
162-
var cacheBatcher = new CacheBatcher(Session);
163164
foreach (var query in _queries)
164165
{
165166
if (query.CachingInformation != null)
166167
{
167-
foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable))
168+
foreach (var cachingInfo in query.CachingInformation)
168169
{
169170
cachingInfo.SetCacheBatcher(cacheBatcher);
170171
}
171172
}
172173

173174
rowCount += query.ProcessResultsSet(reader);
174175
}
175-
cacheBatcher.ExecuteBatch();
176176
}
177177
}
178178

179-
// Query cacheable results must be cached untransformed: the put does not need to wait for
180-
// the ProcessResults.
181-
PutCacheableResults();
182-
183179
foreach (var query in _queries)
184180
{
185-
query.ProcessResults();
181+
//TODO 6.0: Replace with query.ProcessResults();
182+
if (query is IQueryBatchItemWithAsyncProcessResults q)
183+
q.ProcessResults();
184+
else
185+
query.ProcessResults();
186186
}
187+
188+
cacheBatcher?.ExecuteBatch();
189+
PutCacheableResults();
187190
}
188191
catch (Exception sqle)
189192
{

0 commit comments

Comments
 (0)