diff --git a/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs b/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs index 884f2b9d0c4..8a91aea24ff 100644 --- a/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs +++ b/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs @@ -10,6 +10,7 @@ using System.Linq; using NHibernate.Cfg; +using NHibernate.DomainModel.Northwind.Entities; using NHibernate.Linq; using NUnit.Framework; @@ -281,5 +282,140 @@ public async Task CanBeCombinedWithFetchAsync() Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); } + + [Test] + public async Task FetchIsCachableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + Order order; + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + order = (await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToListAsync())) + .First(); + + await (t.CommitAsync()); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + order = (await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToListAsync())) + .First(); + await (t.CommitAsync()); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + + } + + [Test] + public async Task FutureFetchIsCachableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + Order order; + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .Where(x => x.OrderId == 10248) + .ToFuture(); + + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToFuture() + .ToList() + .First(); + + await (t.CommitAsync()); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .Where(x => x.OrderId == 10248) + .ToFuture(); + + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToFuture() + .ToList() + .First(); + + await (t.CommitAsync()); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); + } + + private static void AssertFetchedOrder(Order order) + { + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); + var orderLine = order.OrderLines.First(); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + } } } diff --git a/src/NHibernate.Test/Linq/QueryCacheableTests.cs b/src/NHibernate.Test/Linq/QueryCacheableTests.cs index b4c9babf20e..93f41608fee 100644 --- a/src/NHibernate.Test/Linq/QueryCacheableTests.cs +++ b/src/NHibernate.Test/Linq/QueryCacheableTests.cs @@ -1,5 +1,6 @@ using System.Linq; using NHibernate.Cfg; +using NHibernate.DomainModel.Northwind.Entities; using NHibernate.Linq; using NUnit.Framework; @@ -270,5 +271,140 @@ public void CanBeCombinedWithFetch() Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); } + + [Test] + public void FetchIsCachable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + Order order; + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToList() + .First(); + + t.Commit(); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToList() + .First(); + t.Commit(); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + + } + + [Test] + public void FutureFetchIsCachable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + Order order; + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .Where(x => x.OrderId == 10248) + .ToFuture(); + + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToFuture() + .ToList() + .First(); + + t.Commit(); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = Sfi.OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(x => x.Customer) + .Where(x => x.OrderId == 10248) + .ToFuture(); + + order = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(x => x.OrderLines) + .ThenFetch(x => x.Product) + .ThenFetchMany(x => x.OrderLines) + .Where(x => x.OrderId == 10248) + .ToFuture() + .ToList() + .First(); + + t.Commit(); + } + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); + } + + private static void AssertFetchedOrder(Order order) + { + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); + var orderLine = order.OrderLines.First(); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + } } } diff --git a/src/NHibernate/Async/Cache/StandardQueryCache.cs b/src/NHibernate/Async/Cache/StandardQueryCache.cs index c23a7a25526..bea449d1a6c 100644 --- a/src/NHibernate/Async/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Async/Cache/StandardQueryCache.cs @@ -14,6 +14,7 @@ using System.Linq; using NHibernate.Cfg; using NHibernate.Engine; +using NHibernate.Persister.Collection; using NHibernate.Type; using NHibernate.Util; @@ -296,39 +297,52 @@ private async Task GetResultFromCacheableAsync( var returnType = returnTypes[0]; // Skip first element, it is the timestamp - var rows = new List(cacheable.Count - 1); for (var i = 1; i < cacheable.Count; i++) { - rows.Add(cacheable[i]); + await (returnType.BeforeAssembleAsync(cacheable[i], session, cancellationToken)).ConfigureAwait(false); } - foreach (var row in rows) - { - await (returnType.BeforeAssembleAsync(row, session, cancellationToken)).ConfigureAwait(false); - } - - foreach (var row in rows) + for (var i = 1; i < cacheable.Count; i++) { - result.Add(await (returnType.AssembleAsync(row, session, null, cancellationToken)).ConfigureAwait(false)); + result.Add(await (returnType.AssembleAsync(cacheable[i], session, null, cancellationToken)).ConfigureAwait(false)); } } else { + var collectionIndexes = new Dictionary(); + var nonCollectionTypeIndexes = new List(); + for (var i = 0; i < returnTypes.Length; i++) + { + if (returnTypes[i] is CollectionType collectionType) + { + collectionIndexes.Add(i, session.Factory.GetCollectionPersister(collectionType.Role)); + } + else + { + nonCollectionTypeIndexes.Add(i); + } + } + // Skip first element, it is the timestamp - var rows = new List(cacheable.Count - 1); for (var i = 1; i < cacheable.Count; i++) { - rows.Add((object[]) cacheable[i]); + await (TypeHelper.BeforeAssembleAsync((object[]) cacheable[i], returnTypes, session, cancellationToken)).ConfigureAwait(false); } - foreach (var row in rows) + for (var i = 1; i < cacheable.Count; i++) { - await (TypeHelper.BeforeAssembleAsync(row, returnTypes, session, cancellationToken)).ConfigureAwait(false); + result.Add(await (TypeHelper.AssembleAsync((object[]) cacheable[i], returnTypes, nonCollectionTypeIndexes, session, cancellationToken)).ConfigureAwait(false)); } - foreach (var row in rows) + // Initialization of the fetched collection must be done at the end in order to be able to batch fetch them + // from the cache or database. The collections were already created in the previous for statement so we only + // have to initialize them. + if (collectionIndexes.Count > 0) { - result.Add(await (TypeHelper.AssembleAsync(row, returnTypes, session, null, cancellationToken)).ConfigureAwait(false)); + for (var i = 1; i < cacheable.Count; i++) + { + await (TypeHelper.InitializeCollectionsAsync((object[]) cacheable[i], (object[]) result[i - 1], collectionIndexes, session, cancellationToken)).ConfigureAwait(false); + } } } diff --git a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs index 3f063cf00e6..d7f4ccc38fc 100644 --- a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs @@ -185,8 +185,8 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken object o = await (loader.GetRowFromResultSetAsync(reader, session, queryParameters, loader.GetLockModes(queryParameters.LockModes), - null, hydratedObjects[i], keys, true, - (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); + null, hydratedObjects[i], keys, true, null, null, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { subselectResultKeys[i].Add(keys); diff --git a/src/NHibernate/Async/Impl/MultiQueryImpl.cs b/src/NHibernate/Async/Impl/MultiQueryImpl.cs index b65ead8cf26..4e2f6051deb 100644 --- a/src/NHibernate/Async/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Async/Impl/MultiQueryImpl.cs @@ -143,7 +143,7 @@ protected async Task> DoListAsync(CancellationToken cancellationTok rowCount++; object result = await (translator.Loader.GetRowFromResultSetAsync( - reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, + reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, null, null, (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); tempResults.Add(result); diff --git a/src/NHibernate/Async/Loader/Hql/QueryLoader.cs b/src/NHibernate/Async/Loader/Hql/QueryLoader.cs index a25c9b8c52a..4285de1c77e 100644 --- a/src/NHibernate/Async/Loader/Hql/QueryLoader.cs +++ b/src/NHibernate/Async/Loader/Hql/QueryLoader.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; using NHibernate.Engine; using NHibernate.Event; using NHibernate.Hql.Ast.ANTLR; diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index bf7d06b1046..a87d8845ce3 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -56,11 +56,13 @@ private Task DoQueryAndInitializeNonLazyCollectionsAsync(ISessionImplemen { return Task.FromCanceled(cancellationToken); } - return DoQueryAndInitializeNonLazyCollectionsAsync(session, queryParameters, returnProxies, null, cancellationToken); + return DoQueryAndInitializeNonLazyCollectionsAsync(session, queryParameters, returnProxies, null, null, cancellationToken); } - private async Task DoQueryAndInitializeNonLazyCollectionsAsync(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, IResultTransformer forcedResultTransformer, CancellationToken cancellationToken) + private async Task DoQueryAndInitializeNonLazyCollectionsAsync(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, + IResultTransformer forcedResultTransformer, + QueryCacheResultBuilder queryCacheResultBuilder, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); IPersistenceContext persistenceContext = session.PersistenceContext; @@ -77,7 +79,7 @@ private async Task DoQueryAndInitializeNonLazyCollectionsAsync(ISessionIm { try { - result = await (DoQueryAsync(session, queryParameters, returnProxies, forcedResultTransformer, cancellationToken)).ConfigureAwait(false); + result = await (DoQueryAsync(session, queryParameters, returnProxies, forcedResultTransformer, queryCacheResultBuilder, cancellationToken)).ConfigureAwait(false); } finally { @@ -119,7 +121,8 @@ protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISession { result = await (GetRowFromResultSetAsync(resultSet, session, queryParameters, GetLockModes(queryParameters.LockModes), null, - hydratedObjects, new EntityKey[entitySpan], returnProxies, (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); + hydratedObjects, new EntityKey[entitySpan], returnProxies, null, null, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (HibernateException) @@ -139,24 +142,12 @@ protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISession return result; } - internal Task GetRowFromResultSetAsync(DbDataReader resultSet, ISessionImplementor session, - QueryParameters queryParameters, LockMode[] lockModeArray, - EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies, Action cacheBatchingHandler, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - return GetRowFromResultSetAsync(resultSet, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, null, cacheBatchingHandler, cancellationToken); - } - internal async Task GetRowFromResultSetAsync(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, bool returnProxies, IResultTransformer forcedResultTransformer, - Action cacheBatchingHandler, CancellationToken cancellationToken) + QueryCacheResultBuilder queryCacheResultBuilder, + Action cacheBatchingHandler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ILoadable[] persisters = EntityPersisters; @@ -176,7 +167,7 @@ internal async Task GetRowFromResultSetAsync(DbDataReader resultSet, ISe await (GetRowAsync(resultSet, persisters, keys, queryParameters.OptionalObject, optionalObjectKey, lockModeArray, hydratedObjects, session, !returnProxies, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false); - await (ReadCollectionElementsAsync(row, resultSet, session, cancellationToken)).ConfigureAwait(false); + var collections = await (ReadCollectionElementsAsync(row, resultSet, session, cancellationToken)).ConfigureAwait(false); if (returnProxies) { @@ -205,16 +196,20 @@ internal async Task GetRowFromResultSetAsync(DbDataReader resultSet, ISe } } - return forcedResultTransformer == null + var result = forcedResultTransformer == null ? await (GetResultColumnOrRowAsync(row, queryParameters.ResultTransformer, resultSet, session, cancellationToken)).ConfigureAwait(false) : forcedResultTransformer.TransformTuple(await (GetResultRowAsync(row, resultSet, session, cancellationToken)).ConfigureAwait(false), ResultRowAliases); + + queryCacheResultBuilder?.AddRow(result, row, collections); + + return result; } /// /// Read any collection elements contained in a single row of the result set /// - private async Task ReadCollectionElementsAsync(object[] row, DbDataReader resultSet, ISessionImplementor session, CancellationToken cancellationToken) + private async Task ReadCollectionElementsAsync(object[] row, DbDataReader resultSet, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); //TODO: make this handle multiple collection roles! @@ -223,6 +218,7 @@ private async Task ReadCollectionElementsAsync(object[] row, DbDataReader result if (collectionPersisters != null) { + var result = new IPersistentCollection[collectionPersisters.Length]; ICollectionAliases[] descriptors = CollectionAliases; int[] collectionOwners = CollectionOwners; @@ -249,12 +245,17 @@ private async Task ReadCollectionElementsAsync(object[] row, DbDataReader result //keys[collectionOwner].getIdentifier() } - await (ReadCollectionElementAsync(owner, key, collectionPersister, descriptors[i], resultSet, session, cancellationToken)).ConfigureAwait(false); + result[i] = await (ReadCollectionElementAsync(owner, key, collectionPersister, descriptors[i], resultSet, session, cancellationToken)).ConfigureAwait(false); } + + return result; } + + return null; } - private async Task DoQueryAsync(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, IResultTransformer forcedResultTransformer, CancellationToken cancellationToken) + private async Task DoQueryAsync(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, + IResultTransformer forcedResultTransformer, QueryCacheResultBuilder queryCacheResultBuilder, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (session.BeginProcess()) @@ -302,8 +303,8 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet object result = await (GetRowFromResultSetAsync(rs, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, forcedResultTransformer, - (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); + keys, returnProxies, forcedResultTransformer, queryCacheResultBuilder, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); results.Add(result); if (createSubselects) @@ -476,7 +477,7 @@ protected virtual Task GetResultRowAsync(Object[] row, DbDataReader rs /// /// Read one collection element from the current row of the ADO.NET result set /// - private static async Task ReadCollectionElementAsync(object optionalOwner, object optionalKey, ICollectionPersister persister, + private static async Task ReadCollectionElementAsync(object optionalOwner, object optionalKey, ICollectionPersister persister, ICollectionAliases descriptor, DbDataReader rs, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -513,6 +514,8 @@ private static async Task ReadCollectionElementAsync(object optionalOwner, objec { await (rowCollection.ReadFromAsync(rs, persister, descriptor, owner, cancellationToken)).ConfigureAwait(false); } + + return rowCollection; } else if (optionalKey != null) { @@ -524,11 +527,13 @@ private static async Task ReadCollectionElementAsync(object optionalOwner, objec { Log.Debug("result set contains (possibly empty) collection: {0}", MessageHelper.CollectionInfoString(persister, optionalKey)); } - persistenceContext.LoadContexts.GetCollectionLoadContext(rs).GetLoadingCollection(persister, optionalKey); + // handle empty collection + return persistenceContext.LoadContexts.GetCollectionLoadContext(rs).GetLoadingCollection(persister, optionalKey); } // else no collection element, but also no owner + return null; } /// @@ -1361,13 +1366,18 @@ private async Task ListUsingQueryCacheAsync(ISessionImplementor session, IQueryCache queryCache = _factory.GetQueryCache(queryParameters.CacheRegion); QueryKey key = GenerateQueryKey(session, queryParameters); + var queryCacheBuilder = new QueryCacheResultBuilder(this); IList result = await (GetResultFromQueryCacheAsync(session, queryParameters, querySpaces, queryCache, key, cancellationToken)).ConfigureAwait(false); if (result == null) { - result = await (DoListAsync(session, queryParameters, key.ResultTransformer, cancellationToken)).ConfigureAwait(false); - await (PutResultInQueryCacheAsync(session, queryParameters, queryCache, key, result, cancellationToken)).ConfigureAwait(false); + result = await (DoListAsync(session, queryParameters, key.ResultTransformer, queryCacheBuilder, cancellationToken)).ConfigureAwait(false); + await (PutResultInQueryCacheAsync(session, queryParameters, queryCache, key, queryCacheBuilder.Result, cancellationToken)).ConfigureAwait(false); + } + else + { + result = queryCacheBuilder.GetResultList(result); } result = TransformCacheableResults(queryParameters, key.ResultTransformer, result); @@ -1387,7 +1397,7 @@ private async Task GetResultFromQueryCacheAsync( key, queryParameters, queryParameters.HasAutoDiscoverScalarTypes ? null - : key.ResultTransformer.GetCachedResultTypes(ResultTypes), + : key.ResultTransformer.GetCachedResultTypes(CacheTypes), querySpaces, session, cancellationToken)).ConfigureAwait(false); if (_factory.Statistics.IsStatisticsEnabled) @@ -1414,7 +1424,7 @@ private async Task PutResultInQueryCacheAsync(ISessionImplementor session, Query var put = await (queryCache.PutAsync( key, queryParameters, - key.ResultTransformer.GetCachedResultTypes(ResultTypes), + key.ResultTransformer.GetCachedResultTypes(CacheTypes), result, session, cancellationToken)).ConfigureAwait(false); if (put && _factory.Statistics.IsStatisticsEnabled) @@ -1436,10 +1446,22 @@ protected Task DoListAsync(ISessionImplementor session, QueryParameters q { return Task.FromCanceled(cancellationToken); } - return DoListAsync(session, queryParameters, null, cancellationToken); + return DoListAsync(session, queryParameters, null, null, cancellationToken); + } + + // Since 5.3 + [Obsolete("Use the overload with queryCacheResultBuilder parameter")] + protected Task DoListAsync(ISessionImplementor session, QueryParameters queryParameters, IResultTransformer forcedResultTransformer, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return DoListAsync(session, queryParameters, forcedResultTransformer, null, cancellationToken); } - protected async Task DoListAsync(ISessionImplementor session, QueryParameters queryParameters, IResultTransformer forcedResultTransformer, CancellationToken cancellationToken) + protected async Task DoListAsync(ISessionImplementor session, QueryParameters queryParameters, IResultTransformer forcedResultTransformer, + QueryCacheResultBuilder queryCacheResultBuilder, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); bool statsEnabled = Factory.Statistics.IsStatisticsEnabled; @@ -1452,7 +1474,7 @@ protected async Task DoListAsync(ISessionImplementor session, QueryParame IList result; try { - result = await (DoQueryAndInitializeNonLazyCollectionsAsync(session, queryParameters, true, forcedResultTransformer, cancellationToken)).ConfigureAwait(false); + result = await (DoQueryAndInitializeNonLazyCollectionsAsync(session, queryParameters, true, forcedResultTransformer, queryCacheResultBuilder, cancellationToken)).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (HibernateException) diff --git a/src/NHibernate/Async/Multi/QueryBatch.cs b/src/NHibernate/Async/Multi/QueryBatch.cs index 0e8f03daadb..91903833cd9 100644 --- a/src/NHibernate/Async/Multi/QueryBatch.cs +++ b/src/NHibernate/Async/Multi/QueryBatch.cs @@ -184,9 +184,14 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) resultSetsCommand.Sql); } - if (statsEnabled) + if (!statsEnabled) + { + return; + } + + stopWatch.Stop(); + if (resultSetsCommand.HasQueries) { - stopWatch.Stop(); Session.Factory.StatisticsImplementor.QueryExecuted( resultSetsCommand.Sql.ToString(), rowCount, @@ -214,7 +219,7 @@ private async Task GetCachedResultsAsync(CancellationToken cancellationToken) parameters[i] = queryInfo.Parameters; returnTypes[i] = queryInfo.Parameters.HasAutoDiscoverScalarTypes ? null - : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.GetCacheTypes()); spaces[i] = queryInfo.QuerySpaces; } @@ -222,11 +227,12 @@ private async Task GetCachedResultsAsync(CancellationToken cancellationToken) for (var i = 0; i < queryInfos.Length; i++) { - queryInfos[i].SetCachedResult(results[i]); + var queryInfo = queryInfos[i]; + queryInfo.SetCachedResult(results[i]); if (statisticsEnabled) { - var queryIdentifier = queryInfos[i].QueryIdentifier; + var queryIdentifier = queryInfo.QueryIdentifier; if (results[i] == null) { Session.Factory.StatisticsImplementor.QueryCacheMiss(queryIdentifier, cache.RegionName); @@ -258,7 +264,7 @@ private async Task PutCacheableResultsAsync(CancellationToken cancellationToken) var queryInfo = queryInfos[i]; keys[i] = queryInfo.CacheKey; parameters[i] = queryInfo.Parameters; - returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.GetCacheTypes()); results[i] = queryInfo.ResultToCache; } diff --git a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs index f262161b342..37fbaff3558 100644 --- a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs @@ -75,6 +75,7 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT var lockModeArray = loader.GetLockModes(queryParameters.LockModes); var optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); var tmpResults = new List(); + var queryCacheBuilder = new QueryCacheResultBuilder(loader); var cacheBatcher = queryInfo.CacheBatcher; var ownCacheBatcher = cacheBatcher == null; if (ownCacheBatcher) @@ -95,6 +96,7 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT keys, true, forcedResultTransformer, + queryCacheBuilder, (persister, data) => cacheBatcher.AddToBatch(persister, data) , cancellationToken )).ConfigureAwait(false); if (loader.IsSubselectLoadingEnabled) @@ -108,7 +110,7 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT queryInfo.Result = tmpResults; if (queryInfo.CanPutToCache) - queryInfo.ResultToCache = tmpResults; + queryInfo.ResultToCache = queryCacheBuilder.Result; if (ownCacheBatcher) await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Type/TypeHelper.cs b/src/NHibernate/Async/Type/TypeHelper.cs index 6208289b5b9..3446d115249 100644 --- a/src/NHibernate/Async/Type/TypeHelper.cs +++ b/src/NHibernate/Async/Type/TypeHelper.cs @@ -10,8 +10,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Intercept; +using NHibernate.Persister.Collection; using NHibernate.Properties; using NHibernate.Tuple; @@ -66,6 +69,68 @@ public static async Task AssembleAsync(object[] row, ICacheAssembler[] return assembled; } + /// + /// Apply the operation across a series of values. + /// + /// The cached values. + /// The value types. + /// The indexes of types to assemble. + /// The originating session. + /// A cancellation token that can be used to cancel the work + /// A new array of assembled values. + internal static async Task AssembleAsync( + object[] row, + ICacheAssembler[] types, + IList typeIndexes, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var assembled = new object[row.Length]; + foreach (var i in typeIndexes) + { + var value = row[i]; + if (Equals(LazyPropertyInitializer.UnfetchedProperty, value) || Equals(BackrefPropertyAccessor.Unknown, value)) + { + assembled[i] = value; + } + else + { + assembled[i] = await (types[i].AssembleAsync(row[i], session, null, cancellationToken)).ConfigureAwait(false); + } + } + + return assembled; + } + + /// + /// Initialize collections from the query cached row and update the assembled row. + /// + /// The cached values. + /// The assembled values to update. + /// The dictionary containing collection persisters and their indexes in the parameter as key. + /// The originating session. + /// A cancellation token that can be used to cancel the work + internal static async Task InitializeCollectionsAsync( + object[] cacheRow, + object[] assembleRow, + IDictionary collectionIndexes, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var pair in collectionIndexes) + { + var value = cacheRow[pair.Key]; + if (value == null) + { + continue; + } + + var collection = session.PersistenceContext.GetCollection(new CollectionKey(pair.Value, value)); + await (collection.ForceInitializationAsync(cancellationToken)).ConfigureAwait(false); + assembleRow[pair.Key] = collection; + } + } + /// Apply the operation across a series of values. /// The values /// The value types @@ -90,7 +155,14 @@ public static async Task DisassembleAsync(object[] row, ICacheAssemble } else { - disassembled[i] = await (types[i].DisassembleAsync(row[i], session, owner, cancellationToken)).ConfigureAwait(false); + if (owner == null && row[i] is IPersistentCollection collection) + { + disassembled[i] = await (types[i].DisassembleAsync(row[i], session, collection.Owner, cancellationToken)).ConfigureAwait(false); + } + else + { + disassembled[i] = await (types[i].DisassembleAsync(row[i], session, owner, cancellationToken)).ConfigureAwait(false); + } } } return disassembled; diff --git a/src/NHibernate/Cache/QueryCacheResultBuilder.cs b/src/NHibernate/Cache/QueryCacheResultBuilder.cs new file mode 100644 index 00000000000..f3f145ddad3 --- /dev/null +++ b/src/NHibernate/Cache/QueryCacheResultBuilder.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Collection; +using NHibernate.Engine; +using NHibernate.Persister.Collection; +using NHibernate.Type; + +namespace NHibernate.Cache +{ + /// + /// A builder that builds a list from a query that can be passed to . + /// + public sealed class QueryCacheResultBuilder + { + private readonly IType[] _resultTypes; + private readonly IType[] _cacheTypes; + private readonly List _entityFetchIndexes = new List(); + private readonly List _collectionFetchIndexes = new List(); + private readonly bool _hasFetches; + + internal QueryCacheResultBuilder(Loader.Loader loader) + { + _resultTypes = loader.ResultTypes; + _cacheTypes = loader.CacheTypes; + + if (loader.EntityFetches != null) + { + for (var i = 0; i < loader.EntityFetches.Length; i++) + { + if (loader.EntityFetches[i]) + { + _entityFetchIndexes.Add(i); + } + } + + _hasFetches = _entityFetchIndexes.Count > 0; + } + + if (loader.CollectionFetches == null) + { + return; + } + + for (var i = 0; i < loader.CollectionFetches.Length; i++) + { + if (loader.CollectionFetches[i]) + { + _collectionFetchIndexes.Add(i); + } + } + + _hasFetches = _hasFetches || _collectionFetchIndexes.Count > 0; + } + + internal IList Result { get; } = new List(); + + internal void AddRow(object result, object[] entities, IPersistentCollection[] collections) + { + if (!_hasFetches) + { + Result.Add(result); + return; + } + + var row = new object[_cacheTypes.Length]; + if (_resultTypes.Length == 1) + { + row[0] = result; + } + else + { + Array.Copy((object[]) result, 0, row, 0, _resultTypes.Length); + } + + var i = _resultTypes.Length; + foreach (var index in _entityFetchIndexes) + { + row[i++] = entities[index]; + } + + foreach (var index in _collectionFetchIndexes) + { + row[i++] = collections[index]; + } + + Result.Add(row); + } + + internal IList GetResultList(IList cacheList) + { + if (!_hasFetches) + { + return cacheList; + } + + var result = new List(); + + foreach (object[] cacheRow in cacheList) + { + if (_resultTypes.Length == 1) + { + result.Add(cacheRow[0]); + } + else + { + var row = new object[_resultTypes.Length]; + Array.Copy(cacheRow, 0, row, 0, _resultTypes.Length); + result.Add(row); + } + } + + return result; + } + } +} diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index ee361c1541c..24f99968ac8 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -4,6 +4,7 @@ using System.Linq; using NHibernate.Cfg; using NHibernate.Engine; +using NHibernate.Persister.Collection; using NHibernate.Type; using NHibernate.Util; @@ -330,39 +331,52 @@ private IList GetResultFromCacheable( var returnType = returnTypes[0]; // Skip first element, it is the timestamp - var rows = new List(cacheable.Count - 1); for (var i = 1; i < cacheable.Count; i++) { - rows.Add(cacheable[i]); + returnType.BeforeAssemble(cacheable[i], session); } - foreach (var row in rows) - { - returnType.BeforeAssemble(row, session); - } - - foreach (var row in rows) + for (var i = 1; i < cacheable.Count; i++) { - result.Add(returnType.Assemble(row, session, null)); + result.Add(returnType.Assemble(cacheable[i], session, null)); } } else { + var collectionIndexes = new Dictionary(); + var nonCollectionTypeIndexes = new List(); + for (var i = 0; i < returnTypes.Length; i++) + { + if (returnTypes[i] is CollectionType collectionType) + { + collectionIndexes.Add(i, session.Factory.GetCollectionPersister(collectionType.Role)); + } + else + { + nonCollectionTypeIndexes.Add(i); + } + } + // Skip first element, it is the timestamp - var rows = new List(cacheable.Count - 1); for (var i = 1; i < cacheable.Count; i++) { - rows.Add((object[]) cacheable[i]); + TypeHelper.BeforeAssemble((object[]) cacheable[i], returnTypes, session); } - foreach (var row in rows) + for (var i = 1; i < cacheable.Count; i++) { - TypeHelper.BeforeAssemble(row, returnTypes, session); + result.Add(TypeHelper.Assemble((object[]) cacheable[i], returnTypes, nonCollectionTypeIndexes, session)); } - foreach (var row in rows) + // Initialization of the fetched collection must be done at the end in order to be able to batch fetch them + // from the cache or database. The collections were already created in the previous for statement so we only + // have to initialize them. + if (collectionIndexes.Count > 0) { - result.Add(TypeHelper.Assemble(row, returnTypes, session, null)); + for (var i = 1; i < cacheable.Count; i++) + { + TypeHelper.InitializeCollections((object[]) cacheable[i], (object[]) result[i - 1], collectionIndexes, session); + } } } diff --git a/src/NHibernate/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Impl/MultiCriteriaImpl.cs index 4920acc5306..d3d6be2fd84 100644 --- a/src/NHibernate/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Impl/MultiCriteriaImpl.cs @@ -258,8 +258,8 @@ private void GetResultsFromDatabase(IList results) object o = loader.GetRowFromResultSet(reader, session, queryParameters, loader.GetLockModes(queryParameters.LockModes), - null, hydratedObjects[i], keys, true, - (persister, data) => cacheBatcher.AddToBatch(persister, data)); + null, hydratedObjects[i], keys, true, null, null, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); if (createSubselects[i]) { subselectResultKeys[i].Add(keys); diff --git a/src/NHibernate/Impl/MultiQueryImpl.cs b/src/NHibernate/Impl/MultiQueryImpl.cs index 310e45125b4..481ce1feb94 100644 --- a/src/NHibernate/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Impl/MultiQueryImpl.cs @@ -587,7 +587,7 @@ protected List DoList() rowCount++; object result = translator.Loader.GetRowFromResultSet( - reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, + reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, null, null, (persister, data) => cacheBatcher.AddToBatch(persister, data)); tempResults.Add(result); diff --git a/src/NHibernate/Loader/Hql/QueryLoader.cs b/src/NHibernate/Loader/Hql/QueryLoader.cs index 9936a60968e..6708091d01d 100644 --- a/src/NHibernate/Loader/Hql/QueryLoader.cs +++ b/src/NHibernate/Loader/Hql/QueryLoader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; using NHibernate.Engine; using NHibernate.Event; using NHibernate.Hql.Ast.ANTLR; @@ -43,6 +44,7 @@ public partial class QueryLoader : BasicLoader private readonly NullableDictionary _sqlAliasByEntityAlias = new NullableDictionary(); private int _selectLength; private LockMode[] _defaultLockModes; + private IType[] _cacheTypes; private ISet _uncacheableCollectionPersisters; public QueryLoader(QueryTranslatorImpl queryTranslator, ISessionFactoryImplementor factory, SelectClause selectClause) @@ -201,6 +203,8 @@ protected override ICollectionPersister[] CollectionPersisters get { return _collectionPersisters; } } + public override IType[] CacheTypes => _cacheTypes; + private void Initialize(SelectClause selectClause) { IList fromElementList = selectClause.FromElementsForLoad; @@ -220,6 +224,7 @@ private void Initialize(SelectClause selectClause) _collectionPersisters = new IQueryableCollection[length]; _collectionOwners = new int[length]; _collectionSuffixes = new string[length]; + CollectionFetches = new bool[length]; for (int i = 0; i < length; i++) { @@ -229,6 +234,7 @@ private void Initialize(SelectClause selectClause) // collectionSuffixes[i] = collectionFromElement.getColumnAliasSuffix(); // collectionSuffixes[i] = Integer.toString( i ) + "_"; _collectionSuffixes[i] = collectionFromElement.CollectionSuffix; + CollectionFetches[i] = collectionFromElement.IsFetch; } } @@ -242,6 +248,8 @@ private void Initialize(SelectClause selectClause) _includeInSelect = new bool[size]; _owners = new int[size]; _ownerAssociationTypes = new EntityType[size]; + EntityFetches = new bool[size]; + var cacheTypes = new List(ResultTypes); for (int i = 0; i < size; i++) { @@ -264,6 +272,11 @@ private void Initialize(SelectClause selectClause) _sqlAliasSuffixes[i] = (size == 1) ? "" : i + "_"; // sqlAliasSuffixes[i] = element.getColumnAliasSuffix(); _includeInSelect[i] = !element.IsFetch; + EntityFetches[i] = element.IsFetch; + if (element.IsFetch) + { + cacheTypes.Add(_entityPersisters[i].Type); + } if (_includeInSelect[i]) { _selectLength++; @@ -288,6 +301,13 @@ private void Initialize(SelectClause selectClause) } } + if (_collectionPersisters != null) + { + cacheTypes.AddRange(_collectionPersisters.Where((t, i) => CollectionFetches[i]).Select(t => t.CollectionType)); + } + + _cacheTypes = cacheTypes.ToArray(); + //NONE, because its the requested lock mode, not the actual! _defaultLockModes = ArrayHelper.Fill(LockMode.None, size); _uncacheableCollectionPersisters = _queryTranslator.UncacheableCollectionPersisters; diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index a89c9b9b007..97adc046a84 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -156,6 +156,12 @@ public virtual bool IsSubselectLoadingEnabled /// public IType[] ResultTypes { get; protected set; } + public bool[] EntityFetches { get; protected set; } + + public bool[] CollectionFetches { get; protected set; } + + public virtual IType[] CacheTypes => ResultTypes; + public ISessionFactoryImplementor Factory { get { return _factory; } @@ -254,11 +260,13 @@ private static SqlString PrependComment(SqlString sql, QueryParameters parameter private IList DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies) { - return DoQueryAndInitializeNonLazyCollections(session, queryParameters, returnProxies, null); + return DoQueryAndInitializeNonLazyCollections(session, queryParameters, returnProxies, null, null); } - private IList DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, IResultTransformer forcedResultTransformer) + private IList DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, + IResultTransformer forcedResultTransformer, + QueryCacheResultBuilder queryCacheResultBuilder) { IPersistenceContext persistenceContext = session.PersistenceContext; bool defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; @@ -274,7 +282,7 @@ private IList DoQueryAndInitializeNonLazyCollections(ISessionImplementor session { try { - result = DoQuery(session, queryParameters, returnProxies, forcedResultTransformer); + result = DoQuery(session, queryParameters, returnProxies, forcedResultTransformer, queryCacheResultBuilder); } finally { @@ -314,7 +322,8 @@ protected object LoadSingleRow(DbDataReader resultSet, ISessionImplementor sessi { result = GetRowFromResultSet(resultSet, session, queryParameters, GetLockModes(queryParameters.LockModes), null, - hydratedObjects, new EntityKey[entitySpan], returnProxies, (persister, data) => cacheBatcher.AddToBatch(persister, data)); + hydratedObjects, new EntityKey[entitySpan], returnProxies, null, null, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); } catch (HibernateException) { @@ -351,20 +360,12 @@ internal static EntityKey GetOptionalObjectKey(QueryParameters queryParameters, } } - internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor session, - QueryParameters queryParameters, LockMode[] lockModeArray, - EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies, Action cacheBatchingHandler) - { - return GetRowFromResultSet(resultSet, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, null, cacheBatchingHandler); - } - internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, bool returnProxies, IResultTransformer forcedResultTransformer, - Action cacheBatchingHandler) + QueryCacheResultBuilder queryCacheResultBuilder, + Action cacheBatchingHandler) { ILoadable[] persisters = EntityPersisters; int entitySpan = persisters.Length; @@ -383,7 +384,7 @@ internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor GetRow(resultSet, persisters, keys, queryParameters.OptionalObject, optionalObjectKey, lockModeArray, hydratedObjects, session, !returnProxies, cacheBatchingHandler); - ReadCollectionElements(row, resultSet, session); + var collections = ReadCollectionElements(row, resultSet, session); if (returnProxies) { @@ -412,16 +413,20 @@ internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor } } - return forcedResultTransformer == null + var result = forcedResultTransformer == null ? GetResultColumnOrRow(row, queryParameters.ResultTransformer, resultSet, session) : forcedResultTransformer.TransformTuple(GetResultRow(row, resultSet, session), ResultRowAliases); + + queryCacheResultBuilder?.AddRow(result, row, collections); + + return result; } /// /// Read any collection elements contained in a single row of the result set /// - private void ReadCollectionElements(object[] row, DbDataReader resultSet, ISessionImplementor session) + private IPersistentCollection[] ReadCollectionElements(object[] row, DbDataReader resultSet, ISessionImplementor session) { //TODO: make this handle multiple collection roles! @@ -429,6 +434,7 @@ private void ReadCollectionElements(object[] row, DbDataReader resultSet, ISessi if (collectionPersisters != null) { + var result = new IPersistentCollection[collectionPersisters.Length]; ICollectionAliases[] descriptors = CollectionAliases; int[] collectionOwners = CollectionOwners; @@ -455,12 +461,17 @@ private void ReadCollectionElements(object[] row, DbDataReader resultSet, ISessi //keys[collectionOwner].getIdentifier() } - ReadCollectionElement(owner, key, collectionPersister, descriptors[i], resultSet, session); + result[i] = ReadCollectionElement(owner, key, collectionPersister, descriptors[i], resultSet, session); } + + return result; } + + return null; } - private IList DoQuery(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, IResultTransformer forcedResultTransformer) + private IList DoQuery(ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, + IResultTransformer forcedResultTransformer, QueryCacheResultBuilder queryCacheResultBuilder) { using (session.BeginProcess()) { @@ -507,8 +518,8 @@ private IList DoQuery(ISessionImplementor session, QueryParameters queryParamete object result = GetRowFromResultSet(rs, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, forcedResultTransformer, - (persister, data) => cacheBatcher.AddToBatch(persister, data)); + keys, returnProxies, forcedResultTransformer, queryCacheResultBuilder, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); results.Add(result); if (createSubselects) @@ -794,7 +805,7 @@ private void RegisterNonExists(EntityKey[] keys, ISessionImplementor session) /// /// Read one collection element from the current row of the ADO.NET result set /// - private static void ReadCollectionElement(object optionalOwner, object optionalKey, ICollectionPersister persister, + private static IPersistentCollection ReadCollectionElement(object optionalOwner, object optionalKey, ICollectionPersister persister, ICollectionAliases descriptor, DbDataReader rs, ISessionImplementor session) { IPersistenceContext persistenceContext = session.PersistenceContext; @@ -830,6 +841,8 @@ private static void ReadCollectionElement(object optionalOwner, object optionalK { rowCollection.ReadFrom(rs, persister, descriptor, owner); } + + return rowCollection; } else if (optionalKey != null) { @@ -841,11 +854,13 @@ private static void ReadCollectionElement(object optionalOwner, object optionalK { Log.Debug("result set contains (possibly empty) collection: {0}", MessageHelper.CollectionInfoString(persister, optionalKey)); } - persistenceContext.LoadContexts.GetCollectionLoadContext(rs).GetLoadingCollection(persister, optionalKey); + // handle empty collection + return persistenceContext.LoadContexts.GetCollectionLoadContext(rs).GetLoadingCollection(persister, optionalKey); } // else no collection element, but also no owner + return null; } /// @@ -1811,13 +1826,18 @@ private IList ListUsingQueryCache(ISessionImplementor session, QueryParameters q IQueryCache queryCache = _factory.GetQueryCache(queryParameters.CacheRegion); QueryKey key = GenerateQueryKey(session, queryParameters); + var queryCacheBuilder = new QueryCacheResultBuilder(this); IList result = GetResultFromQueryCache(session, queryParameters, querySpaces, queryCache, key); if (result == null) { - result = DoList(session, queryParameters, key.ResultTransformer); - PutResultInQueryCache(session, queryParameters, queryCache, key, result); + result = DoList(session, queryParameters, key.ResultTransformer, queryCacheBuilder); + PutResultInQueryCache(session, queryParameters, queryCache, key, queryCacheBuilder.Result); + } + else + { + result = queryCacheBuilder.GetResultList(result); } result = TransformCacheableResults(queryParameters, key.ResultTransformer, result); @@ -1866,7 +1886,7 @@ private IList GetResultFromQueryCache( key, queryParameters, queryParameters.HasAutoDiscoverScalarTypes ? null - : key.ResultTransformer.GetCachedResultTypes(ResultTypes), + : key.ResultTransformer.GetCachedResultTypes(CacheTypes), querySpaces, session); if (_factory.Statistics.IsStatisticsEnabled) @@ -1892,7 +1912,7 @@ private void PutResultInQueryCache(ISessionImplementor session, QueryParameters var put = queryCache.Put( key, queryParameters, - key.ResultTransformer.GetCachedResultTypes(ResultTypes), + key.ResultTransformer.GetCachedResultTypes(CacheTypes), result, session); if (put && _factory.Statistics.IsStatisticsEnabled) @@ -1909,10 +1929,18 @@ private void PutResultInQueryCache(ISessionImplementor session, QueryParameters /// protected IList DoList(ISessionImplementor session, QueryParameters queryParameters) { - return DoList(session, queryParameters, null); + return DoList(session, queryParameters, null, null); } + // Since 5.3 + [Obsolete("Use the overload with queryCacheResultBuilder parameter")] protected IList DoList(ISessionImplementor session, QueryParameters queryParameters, IResultTransformer forcedResultTransformer) + { + return DoList(session, queryParameters, forcedResultTransformer, null); + } + + protected IList DoList(ISessionImplementor session, QueryParameters queryParameters, IResultTransformer forcedResultTransformer, + QueryCacheResultBuilder queryCacheResultBuilder) { bool statsEnabled = Factory.Statistics.IsStatisticsEnabled; var stopWatch = new Stopwatch(); @@ -1924,7 +1952,7 @@ protected IList DoList(ISessionImplementor session, QueryParameters queryParamet IList result; try { - result = DoQueryAndInitializeNonLazyCollections(session, queryParameters, true, forcedResultTransformer); + result = DoQueryAndInitializeNonLazyCollections(session, queryParameters, true, forcedResultTransformer, queryCacheResultBuilder); } catch (HibernateException) { diff --git a/src/NHibernate/Multi/ICachingInformation.cs b/src/NHibernate/Multi/ICachingInformation.cs index 906d79ac5ee..5355c6498a8 100644 --- a/src/NHibernate/Multi/ICachingInformation.cs +++ b/src/NHibernate/Multi/ICachingInformation.cs @@ -1,8 +1,12 @@ +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using NHibernate.Cache; using NHibernate.Engine; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate.Multi { @@ -44,6 +48,8 @@ public interface ICachingInformation /// /// The query result types. /// + // Since 5.3 + [Obsolete("This property is not used and will be removed in a future version.")] IType[] ResultTypes { get; } /// @@ -68,4 +74,20 @@ public interface ICachingInformation /// A cache batcher. void SetCacheBatcher(CacheBatcher cacheBatcher); } + + internal static class CachingInformationExtensions + { + // 6.0 TODO: Remove and use CacheTypes instead. + public static IType[] GetCacheTypes(this ICachingInformation cachingInformation) + { + if (cachingInformation is ICachingInformationWithFetches cachingInformationWithFetches) + { + return cachingInformationWithFetches.CacheTypes; + } + +#pragma warning disable 618 + return cachingInformation.ResultTypes; +#pragma warning restore 618 + } + } } diff --git a/src/NHibernate/Multi/ICachingInformationWithFetches.cs b/src/NHibernate/Multi/ICachingInformationWithFetches.cs new file mode 100644 index 00000000000..5ef73d37627 --- /dev/null +++ b/src/NHibernate/Multi/ICachingInformationWithFetches.cs @@ -0,0 +1,13 @@ +using NHibernate.Type; + +namespace NHibernate.Multi +{ + // 6.0 TODO: merge into 'ICachingInformation'. + internal interface ICachingInformationWithFetches + { + /// + /// The query cache types. + /// + IType[] CacheTypes { get; } + } +} diff --git a/src/NHibernate/Multi/QueryBatch.cs b/src/NHibernate/Multi/QueryBatch.cs index ff25cd5076e..0543f3fa684 100644 --- a/src/NHibernate/Multi/QueryBatch.cs +++ b/src/NHibernate/Multi/QueryBatch.cs @@ -195,9 +195,14 @@ protected void ExecuteBatched() resultSetsCommand.Sql); } - if (statsEnabled) + if (!statsEnabled) + { + return; + } + + stopWatch.Stop(); + if (resultSetsCommand.HasQueries) { - stopWatch.Stop(); Session.Factory.StatisticsImplementor.QueryExecuted( resultSetsCommand.Sql.ToString(), rowCount, @@ -224,7 +229,7 @@ private void GetCachedResults() parameters[i] = queryInfo.Parameters; returnTypes[i] = queryInfo.Parameters.HasAutoDiscoverScalarTypes ? null - : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.GetCacheTypes()); spaces[i] = queryInfo.QuerySpaces; } @@ -232,11 +237,12 @@ private void GetCachedResults() for (var i = 0; i < queryInfos.Length; i++) { - queryInfos[i].SetCachedResult(results[i]); + var queryInfo = queryInfos[i]; + queryInfo.SetCachedResult(results[i]); if (statisticsEnabled) { - var queryIdentifier = queryInfos[i].QueryIdentifier; + var queryIdentifier = queryInfo.QueryIdentifier; if (results[i] == null) { Session.Factory.StatisticsImplementor.QueryCacheMiss(queryIdentifier, cache.RegionName); @@ -276,7 +282,7 @@ private void PutCacheableResults() var queryInfo = queryInfos[i]; keys[i] = queryInfo.CacheKey; parameters[i] = queryInfo.Parameters; - returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.GetCacheTypes()); results[i] = queryInfo.ResultToCache; } diff --git a/src/NHibernate/Multi/QueryBatchItemBase.cs b/src/NHibernate/Multi/QueryBatchItemBase.cs index 73d9cafd18e..6655a88503d 100644 --- a/src/NHibernate/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Multi/QueryBatchItemBase.cs @@ -22,7 +22,7 @@ public abstract partial class QueryBatchItemBase : IQueryBatchItem _finalResults; - protected class QueryInfo : ICachingInformation + protected class QueryInfo : ICachingInformation, ICachingInformationWithFetches { /// /// The query loader. @@ -56,6 +56,9 @@ protected class QueryInfo : ICachingInformation /// public IType[] ResultTypes => Loader.ResultTypes; + /// + public IType[] CacheTypes => Loader.CacheTypes; + /// public string QueryIdentifier => Loader.QueryIdentifier; @@ -214,6 +217,7 @@ public int ProcessResultsSet(DbDataReader reader) var lockModeArray = loader.GetLockModes(queryParameters.LockModes); var optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); var tmpResults = new List(); + var queryCacheBuilder = new QueryCacheResultBuilder(loader); var cacheBatcher = queryInfo.CacheBatcher; var ownCacheBatcher = cacheBatcher == null; if (ownCacheBatcher) @@ -234,6 +238,7 @@ public int ProcessResultsSet(DbDataReader reader) keys, true, forcedResultTransformer, + queryCacheBuilder, (persister, data) => cacheBatcher.AddToBatch(persister, data) ); if (loader.IsSubselectLoadingEnabled) @@ -247,7 +252,7 @@ public int ProcessResultsSet(DbDataReader reader) queryInfo.Result = tmpResults; if (queryInfo.CanPutToCache) - queryInfo.ResultToCache = tmpResults; + queryInfo.ResultToCache = queryCacheBuilder.Result; if (ownCacheBatcher) cacheBatcher.ExecuteBatch(); @@ -276,6 +281,12 @@ public void ProcessResults() if (queryInfo.IsCacheable) { + if (queryInfo.IsResultFromCache) + { + var queryCacheBuilder = new QueryCacheResultBuilder(queryInfo.Loader); + queryInfo.Result = queryCacheBuilder.GetResultList(queryInfo.Result); + } + // This transformation must not be applied to ResultToCache. queryInfo.Result = queryInfo.Loader.TransformCacheableResults( diff --git a/src/NHibernate/Type/TypeHelper.cs b/src/NHibernate/Type/TypeHelper.cs index 56a676cc87a..1e28e73c8a5 100644 --- a/src/NHibernate/Type/TypeHelper.cs +++ b/src/NHibernate/Type/TypeHelper.cs @@ -1,7 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; +using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Intercept; +using NHibernate.Persister.Collection; using NHibernate.Properties; using NHibernate.Tuple; @@ -78,6 +81,64 @@ public static object[] Assemble(object[] row, ICacheAssembler[] types, ISessionI return assembled; } + /// + /// Apply the operation across a series of values. + /// + /// The cached values. + /// The value types. + /// The indexes of types to assemble. + /// The originating session. + /// A new array of assembled values. + internal static object[] Assemble( + object[] row, + ICacheAssembler[] types, + IList typeIndexes, + ISessionImplementor session) + { + var assembled = new object[row.Length]; + foreach (var i in typeIndexes) + { + var value = row[i]; + if (Equals(LazyPropertyInitializer.UnfetchedProperty, value) || Equals(BackrefPropertyAccessor.Unknown, value)) + { + assembled[i] = value; + } + else + { + assembled[i] = types[i].Assemble(row[i], session, null); + } + } + + return assembled; + } + + /// + /// Initialize collections from the query cached row and update the assembled row. + /// + /// The cached values. + /// The assembled values to update. + /// The dictionary containing collection persisters and their indexes in the parameter as key. + /// The originating session. + internal static void InitializeCollections( + object[] cacheRow, + object[] assembleRow, + IDictionary collectionIndexes, + ISessionImplementor session) + { + foreach (var pair in collectionIndexes) + { + var value = cacheRow[pair.Key]; + if (value == null) + { + continue; + } + + var collection = session.PersistenceContext.GetCollection(new CollectionKey(pair.Value, value)); + collection.ForceInitialization(); + assembleRow[pair.Key] = collection; + } + } + /// Apply the operation across a series of values. /// The values /// The value types @@ -100,7 +161,14 @@ public static object[] Disassemble(object[] row, ICacheAssembler[] types, bool[] } else { - disassembled[i] = types[i].Disassemble(row[i], session, owner); + if (owner == null && row[i] is IPersistentCollection collection) + { + disassembled[i] = types[i].Disassemble(row[i], session, collection.Owner); + } + else + { + disassembled[i] = types[i].Disassemble(row[i], session, owner); + } } } return disassembled;