diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 382d07d44c9..b12437b419c 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -5,6 +5,10 @@ applyChanges: true analyzation: methodConversion: +#TODO 6.0: Remove ignore rule for IQueryBatchItem.ProcessResults + - conversion: Ignore + name: ProcessResults + containingTypeName: IQueryBatchItem - conversion: Ignore name: PostProcessInsert containingTypeName: HqlSqlWalker diff --git a/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs b/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs index 2fe7614ce42..1c4a39aae97 100644 --- a/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs +++ b/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs @@ -498,6 +498,35 @@ public async Task CacheModeWorksWithFutureAsync() } } + //GH-2173 + [Test] + public async Task CanFetchNonLazyEntitiesInSubsequentQueryAsync() + { + Sfi.Statistics.IsStatisticsEnabled = true; + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + await (s.SaveAsync( + new EntityEager + { + Name = "EagerManyToOneAssociation", + EagerEntity = new EntityEagerChild {Name = "association"} + })); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + { + Sfi.Statistics.Clear(); + //EntityEager.EagerEntity is lazy initialized instead of being loaded by the second query + s.QueryOver().Fetch(SelectMode.Skip, x => x.EagerEntity).Future(); + await (s.QueryOver().Fetch(SelectMode.Fetch, x => x.EagerEntity).Future().GetEnumerableAsync()); + + if(SupportsMultipleQueries) + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + } + } + #region Test Setup protected override HbmMapping GetMappings() @@ -543,6 +572,10 @@ protected override HbmMapping GetMappings() rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); rc.Property(x => x.Name); + rc.ManyToOne(x => x.EagerEntity, m => + { + m.Cascade(Mapping.ByCode.Cascade.Persist); + }); rc.Bag(ep => ep.ChildrenListSubselect, m => { @@ -560,6 +593,14 @@ protected override HbmMapping GetMappings() }, a => a.OneToMany()); }); + mapper.Class( + rc => + { + rc.Lazy(false); + + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); mapper.Class( rc => { diff --git a/src/NHibernate.Test/Futures/Entities.cs b/src/NHibernate.Test/Futures/Entities.cs index 3c866f497fd..c68a0574850 100644 --- a/src/NHibernate.Test/Futures/Entities.cs +++ b/src/NHibernate.Test/Futures/Entities.cs @@ -36,11 +36,18 @@ public class EntitySubselectChild public virtual EntityEager Parent { get; set; } } + public class EntityEagerChild + { + public Guid Id { get; set; } + public string Name { get; set; } + } + public class EntityEager { public Guid Id { get; set; } public string Name { get; set; } + public EntityEagerChild EagerEntity { get; set; } public IList ChildrenListSubselect { get; set; } public IList ChildrenListEager { get; set; } //= new HashSet(); } diff --git a/src/NHibernate.Test/Futures/QueryBatchFixture.cs b/src/NHibernate.Test/Futures/QueryBatchFixture.cs index cb79faedae1..0f959a54559 100644 --- a/src/NHibernate.Test/Futures/QueryBatchFixture.cs +++ b/src/NHibernate.Test/Futures/QueryBatchFixture.cs @@ -486,6 +486,35 @@ public void CacheModeWorksWithFuture() } } + //GH-2173 + [Test] + public void CanFetchNonLazyEntitiesInSubsequentQuery() + { + Sfi.Statistics.IsStatisticsEnabled = true; + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Save( + new EntityEager + { + Name = "EagerManyToOneAssociation", + EagerEntity = new EntityEagerChild {Name = "association"} + }); + t.Commit(); + } + + using (var s = OpenSession()) + { + Sfi.Statistics.Clear(); + //EntityEager.EagerEntity is lazy initialized instead of being loaded by the second query + s.QueryOver().Fetch(SelectMode.Skip, x => x.EagerEntity).Future(); + s.QueryOver().Fetch(SelectMode.Fetch, x => x.EagerEntity).Future().GetEnumerable(); + + if(SupportsMultipleQueries) + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + } + } + #region Test Setup protected override HbmMapping GetMappings() @@ -531,6 +560,10 @@ protected override HbmMapping GetMappings() rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); rc.Property(x => x.Name); + rc.ManyToOne(x => x.EagerEntity, m => + { + m.Cascade(Mapping.ByCode.Cascade.Persist); + }); rc.Bag(ep => ep.ChildrenListSubselect, m => { @@ -548,6 +581,14 @@ protected override HbmMapping GetMappings() }, a => a.OneToMany()); }); + mapper.Class( + rc => + { + rc.Lazy(false); + + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); mapper.Class( rc => { diff --git a/src/NHibernate/Async/Multi/IQueryBatchItem.cs b/src/NHibernate/Async/Multi/IQueryBatchItem.cs index 257f6673964..c8e4fa5a2a1 100644 --- a/src/NHibernate/Async/Multi/IQueryBatchItem.cs +++ b/src/NHibernate/Async/Multi/IQueryBatchItem.cs @@ -36,4 +36,9 @@ public partial interface IQueryBatchItem /// A cancellation token that can be used to cancel the work Task ExecuteNonBatchedAsync(CancellationToken cancellationToken); } + + internal partial interface IQueryBatchItemWithAsyncProcessResults + { + Task ProcessResultsAsync(CancellationToken cancellationToken); + } } diff --git a/src/NHibernate/Async/Multi/QueryBatch.cs b/src/NHibernate/Async/Multi/QueryBatch.cs index e1faf9c5b47..b27f174665e 100644 --- a/src/NHibernate/Async/Multi/QueryBatch.cs +++ b/src/NHibernate/Async/Multi/QueryBatch.cs @@ -127,18 +127,19 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) } var rowCount = 0; + CacheBatcher cacheBatcher = null; try { if (resultSetsCommand.HasQueries) { + cacheBatcher = new CacheBatcher(Session); using (var reader = await (resultSetsCommand.GetReaderAsync(Timeout, cancellationToken)).ConfigureAwait(false)) { - var cacheBatcher = new CacheBatcher(Session); foreach (var query in _queries) { if (query.CachingInformation != null) { - foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable)) + foreach (var cachingInfo in query.CachingInformation) { cachingInfo.SetCacheBatcher(cacheBatcher); } @@ -146,18 +147,28 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) rowCount += await (query.ProcessResultsSetAsync(reader, cancellationToken)).ConfigureAwait(false); } - await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } } - // Query cacheable results must be cached untransformed: the put does not need to wait for - // the ProcessResults. - await (PutCacheableResultsAsync(cancellationToken)).ConfigureAwait(false); - foreach (var query in _queries) { - query.ProcessResults(); + //TODO 6.0: Replace with query.ProcessResults(); + if (query is IQueryBatchItemWithAsyncProcessResults q) + await (q.ProcessResultsAsync(cancellationToken)).ConfigureAwait(false); + else + query.ProcessResults(); } + + var executeBatchTask = cacheBatcher?.ExecuteBatchAsync(cancellationToken); + + if (executeBatchTask != null) + + { + + await (executeBatchTask).ConfigureAwait(false); + + } + await (PutCacheableResultsAsync(cancellationToken)).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception sqle) diff --git a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs index 47e7bf0a952..cd144051e0f 100644 --- a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs @@ -23,7 +23,7 @@ namespace NHibernate.Multi { using System.Threading.Tasks; using System.Threading; - public abstract partial class QueryBatchItemBase : IQueryBatchItem + public abstract partial class QueryBatchItemBase : IQueryBatchItem, IQueryBatchItemWithAsyncProcessResults { /// @@ -103,17 +103,46 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT queryInfo.Result = tmpResults; if (queryInfo.CanPutToCache) - queryInfo.ResultToCache = tmpResults; + queryInfo.ResultToCache = new List(tmpResults); await (reader.NextResultAsync(cancellationToken)).ConfigureAwait(false); } - await (InitializeEntitiesAndCollectionsAsync(reader, hydratedObjects, cancellationToken)).ConfigureAwait(false); - + StopLoadingCollections(reader); + _reader = reader; + _hydratedObjects = hydratedObjects; return rowCount; } } + /// + public async Task ProcessResultsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfNotInitialized(); + + using (Session.SwitchCacheMode(_cacheMode)) + await (InitializeEntitiesAndCollectionsAsync(_reader, _hydratedObjects, cancellationToken)).ConfigureAwait(false); + + for (var i = 0; i < _queryInfos.Count; i++) + { + var queryInfo = _queryInfos[i]; + if (_subselectResultKeys[i] != null) + { + queryInfo.Loader.CreateSubselects(_subselectResultKeys[i], queryInfo.Parameters, Session); + } + + if (queryInfo.IsCacheable) + { + // This transformation must not be applied to ResultToCache. + queryInfo.Result = + queryInfo.Loader.TransformCacheableResults( + queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, queryInfo.Result); + } + } + AfterLoadCallback?.Invoke(GetResults()); + } + /// public async Task ExecuteNonBatchedAsync(CancellationToken cancellationToken) { diff --git a/src/NHibernate/Engine/Loading/CollectionLoadContext.cs b/src/NHibernate/Engine/Loading/CollectionLoadContext.cs index e41b0f9f404..4dded16d234 100644 --- a/src/NHibernate/Engine/Loading/CollectionLoadContext.cs +++ b/src/NHibernate/Engine/Loading/CollectionLoadContext.cs @@ -124,6 +124,8 @@ public IPersistentCollection GetLoadingCollection(ICollectionPersister persister } else { + if (loadingCollectionEntry.StopLoading) + return null; if (loadingCollectionEntry.ResultSet == resultSet) { log.Debug("found loading collection bound to current result set processing; reading row"); @@ -398,5 +400,17 @@ public override string ToString() { return base.ToString() + ""; } + + internal void StopLoadingCollections(ICollectionPersister[] collectionPersisters) + { + foreach (var collectionKey in localLoadingCollectionKeys) + { + var loadingCollectionEntry = LoadContext.LocateLoadingCollectionEntry(collectionKey); + if (loadingCollectionEntry != null && Array.IndexOf(collectionPersisters, loadingCollectionEntry.Persister) >= 0) + { + loadingCollectionEntry.StopLoading = true; + } + } + } } } diff --git a/src/NHibernate/Engine/Loading/LoadingCollectionEntry.cs b/src/NHibernate/Engine/Loading/LoadingCollectionEntry.cs index 992abf1ae5e..78b0260552a 100644 --- a/src/NHibernate/Engine/Loading/LoadingCollectionEntry.cs +++ b/src/NHibernate/Engine/Loading/LoadingCollectionEntry.cs @@ -44,6 +44,8 @@ public IPersistentCollection Collection get { return collection; } } + public bool StopLoading { get; set; } + public override string ToString() { return GetType().FullName + "@" + Convert.ToString(GetHashCode(), 16); diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index d50d0200dcc..7b3b79fc12a 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -672,6 +672,18 @@ internal void InitializeEntitiesAndCollections( } } + /// + /// Stops further collection population without actual collection initialization. + /// + internal void StopLoadingCollections(ISessionImplementor session, DbDataReader reader) + { + var collectionPersisters = CollectionPersisters; + if (collectionPersisters == null || collectionPersisters.Length == 0) + return; + + session.PersistenceContext.LoadContexts.GetCollectionLoadContext(reader).StopLoadingCollections(collectionPersisters); + } + private void EndCollectionLoad(DbDataReader reader, ISessionImplementor session, ICollectionPersister collectionPersister) { //this is a query and we are loading multiple instances of the same collection role diff --git a/src/NHibernate/Multi/IQueryBatchItem.cs b/src/NHibernate/Multi/IQueryBatchItem.cs index 6e69f1d15f1..0eb11051ae0 100644 --- a/src/NHibernate/Multi/IQueryBatchItem.cs +++ b/src/NHibernate/Multi/IQueryBatchItem.cs @@ -80,4 +80,10 @@ public partial interface IQueryBatchItem /// void ExecuteNonBatched(); } + + //TODO 6.0: Remove along with ignore rule for IQueryBatchItem.ProcessResults in AsyncGenerator.yml + internal partial interface IQueryBatchItemWithAsyncProcessResults + { + void ProcessResults(); + } } diff --git a/src/NHibernate/Multi/QueryBatch.cs b/src/NHibernate/Multi/QueryBatch.cs index ff25cd5076e..915ac0d02da 100644 --- a/src/NHibernate/Multi/QueryBatch.cs +++ b/src/NHibernate/Multi/QueryBatch.cs @@ -153,18 +153,19 @@ protected void ExecuteBatched() } var rowCount = 0; + CacheBatcher cacheBatcher = null; try { if (resultSetsCommand.HasQueries) { + cacheBatcher = new CacheBatcher(Session); using (var reader = resultSetsCommand.GetReader(Timeout)) { - var cacheBatcher = new CacheBatcher(Session); foreach (var query in _queries) { if (query.CachingInformation != null) { - foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable)) + foreach (var cachingInfo in query.CachingInformation) { cachingInfo.SetCacheBatcher(cacheBatcher); } @@ -172,18 +173,20 @@ protected void ExecuteBatched() rowCount += query.ProcessResultsSet(reader); } - cacheBatcher.ExecuteBatch(); } } - // Query cacheable results must be cached untransformed: the put does not need to wait for - // the ProcessResults. - PutCacheableResults(); - foreach (var query in _queries) { - query.ProcessResults(); + //TODO 6.0: Replace with query.ProcessResults(); + if (query is IQueryBatchItemWithAsyncProcessResults q) + q.ProcessResults(); + else + query.ProcessResults(); } + + cacheBatcher?.ExecuteBatch(); + PutCacheableResults(); } catch (Exception sqle) { diff --git a/src/NHibernate/Multi/QueryBatchItemBase.cs b/src/NHibernate/Multi/QueryBatchItemBase.cs index 273070d3f15..fa33182327e 100644 --- a/src/NHibernate/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Multi/QueryBatchItemBase.cs @@ -14,13 +14,15 @@ namespace NHibernate.Multi /// /// Base class for both ICriteria and IQuery queries /// - public abstract partial class QueryBatchItemBase : IQueryBatchItem + public abstract partial class QueryBatchItemBase : IQueryBatchItem, IQueryBatchItemWithAsyncProcessResults { protected ISessionImplementor Session; private List[] _subselectResultKeys; private List _queryInfos; private CacheMode? _cacheMode; private IList _finalResults; + private DbDataReader _reader; + private List[] _hydratedObjects; protected class QueryInfo : ICachingInformation { @@ -242,22 +244,26 @@ public int ProcessResultsSet(DbDataReader reader) queryInfo.Result = tmpResults; if (queryInfo.CanPutToCache) - queryInfo.ResultToCache = tmpResults; + queryInfo.ResultToCache = new List(tmpResults); reader.NextResult(); } - InitializeEntitiesAndCollections(reader, hydratedObjects); - + StopLoadingCollections(reader); + _reader = reader; + _hydratedObjects = hydratedObjects; return rowCount; } } - /// + /// public void ProcessResults() { ThrowIfNotInitialized(); + using (Session.SwitchCacheMode(_cacheMode)) + InitializeEntitiesAndCollections(_reader, _hydratedObjects); + for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; @@ -329,6 +335,17 @@ private void InitializeEntitiesAndCollections(DbDataReader reader, List[ } } + private void StopLoadingCollections(DbDataReader reader) + { + for (var i = 0; i < _queryInfos.Count; i++) + { + var queryInfo = _queryInfos[i]; + if (queryInfo.IsResultFromCache) + continue; + queryInfo.Loader.StopLoadingCollections(Session, reader); + } + } + private void ThrowIfNotInitialized() { if (_queryInfos == null)