diff --git a/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs new file mode 100644 index 00000000000..01403997a46 --- /dev/null +++ b/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs @@ -0,0 +1,536 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Caches.CoreDistributedCache; +using NHibernate.Caches.CoreDistributedCache.Memory; +using NHibernate.Caches.Util.JsonSerializer; +using NHibernate.Cfg; +using NHibernate.Linq; +using NHibernate.Multi; +using NHibernate.Transform; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.CacheTest +{ + using System.Threading.Tasks; + [TestFixture] + public class JsonSerializerCacheFixtureAsync : TestCase + { + protected override string[] Mappings => new[] + { + "CacheTest.ReadOnly.hbm.xml", + "CacheTest.ReadWrite.hbm.xml" + }; + + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.UseSecondLevelCache, "true"); + configuration.SetProperty(Environment.UseQueryCache, "true"); + configuration.SetProperty(Environment.GenerateStatistics, "true"); + configuration.SetProperty(Environment.CacheProvider, typeof(CoreDistributedCacheProvider).AssemblyQualifiedName); + CoreDistributedCacheProvider.CacheFactory = new MemoryFactory(); + var serializer = new JsonCacheSerializer(); + serializer.RegisterType(typeof(Tuple), "tso"); + CoreDistributedCacheProvider.DefaultSerializer = serializer; + } + + protected override void OnSetUp() + { + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + var totalItems = 6; + for (var i = 1; i <= totalItems; i++) + { + var parent = new ReadOnly + { + Name = $"Name{i}" + }; + for (var j = 1; j <= totalItems; j++) + { + var child = new ReadOnlyItem + { + Parent = parent + }; + parent.Items.Add(child); + } + s.Save(parent); + } + for (var i = 1; i <= totalItems; i++) + { + var parent = new ReadWrite + { + Name = $"Name{i}" + }; + for (var j = 1; j <= totalItems; j++) + { + var child = new ReadWriteItem + { + Parent = parent + }; + parent.Items.Add(child); + } + s.Save(parent); + } + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from ReadOnlyItem").ExecuteUpdate(); + s.CreateQuery("delete from ReadWriteItem").ExecuteUpdate(); + s.CreateQuery("delete from ReadOnly").ExecuteUpdate(); + s.CreateQuery("delete from ReadWrite").ExecuteUpdate(); + tx.Commit(); + } + // Must rebuild the session factory, CoreDistribted cache being not clearable. + RebuildSessionFactory(); + } + + [Test] + public async Task CacheableScalarSqlQueryWithTransformerAsync() + { + async Task AssertQueryAsync(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = await (s.CreateSQLQuery("select ro.Name as RegionCode from ReadOnly ro") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .ListAsync()); + await (t.CommitAsync()); + + Assert.That(l.Count, Is.EqualTo(6)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); + } + + [Test] + public async Task CacheableScalarSqlMultiQueryWithTransformerAsync() + { + async Task AssertQueryAsync(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select rw.Name as RegionCode from ReadWrite rw") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select rw.Id as OrgId from ReadWrite rw") + .AddScalar("orgId", NHibernateUtil.Int64) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + await (batch.ExecuteAsync()); + + var l1 = await (batch.GetResultAsync(0)); + var l2 = await (batch.GetResultAsync(1)); + + await (t.CommitAsync()); + + Assert.That(l1.Count, Is.EqualTo(6), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(6), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); + } + + class ResultDto + { + public long OrgId { get; set; } + public string RegionCode { get; set; } + } + + [Test] + public async Task QueryCacheTestAsync() + { + // QueryCache batching is used by QueryBatch. + if (!Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + + Sfi.Statistics.Clear(); + + using var s = OpenSession(); + + const string query = "from ReadOnly e where e.Name = :name"; + const string name1 = "Name1"; + const string name2 = "Name2"; + const string name3 = "Name3"; + const string name4 = "Name4"; + const string name5 = "Name5"; + var q1 = + s + .CreateQuery(query) + .SetString("name", name1) + .SetCacheable(true); + var q2 = + s + .CreateQuery(query) + .SetString("name", name2) + .SetCacheable(true); + var q3 = + s + .Query() + .Where(r => r.Name == name3) + .WithOptions(o => o.SetCacheable(true)); + var q4 = + s + .QueryOver() + .Where(r => r.Name == name4) + .Cacheable(); + var q5 = + s + .CreateSQLQuery("select * " + query) + .AddEntity(typeof(ReadOnly)) + .SetString("name", name5) + .SetCacheable(true); + + var queries = + s + .CreateQueryBatch() + .Add(q1) + .Add(q2) + .Add(q3) + .Add(q4) + .Add(q5); + + using (var t = s.BeginTransaction()) + { + await (queries.ExecuteAsync()); + await (t.CommitAsync()); + } + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "queries first execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(5), "cache misses first execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "cache hits first execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "cache puts first execution"); + + // Run a second time, to test the query cache + using (var t = s.BeginTransaction()) + { + await (queries.ExecuteAsync()); + await (t.CommitAsync()); + } + + Assert.That( + await (queries.GetResultAsync(0)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1"); + Assert.That( + await (queries.GetResultAsync(1)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2"); + Assert.That( + await (queries.GetResultAsync(2)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name3), "q3"); + Assert.That( + await (queries.GetResultAsync(3)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4"); + Assert.That( + await (queries.GetResultAsync(4)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "queries second execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(5), "cache misses second execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(5), "cache hits second execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "cache puts second execution"); + + // Update an entity to invalidate them + using (var t = s.BeginTransaction()) + { + var readwrite1 = await (s.Query().SingleAsync(e => e.Name == name3)); + readwrite1.Name = "NewName"; + await (t.CommitAsync()); + } + + // Run a third time, to re-test the query cache + using (var t = s.BeginTransaction()) + { + await (queries.ExecuteAsync()); + await (t.CommitAsync()); + } + + Assert.That( + await (queries.GetResultAsync(0)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1 after update"); + Assert.That( + await (queries.GetResultAsync(1)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2 after update"); + Assert.That( + await (queries.GetResultAsync(2)), + Has.Count.EqualTo(0), "q3 after update"); + Assert.That( + await (queries.GetResultAsync(3)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4 after update"); + Assert.That( + await (queries.GetResultAsync(4)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5 after update"); + + // The two ReadWrite queries should have been re-executed, so count should have been incremented accordingly. + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(3), "queries third execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(7), "cache misses third execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(8), "cache hits third execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(7), "cache puts third execution"); + } + + [Test] + public async Task QueryEntityBatchCacheTestAsync() + { + Sfi.Statistics.Clear(); + + List items; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .ToListAsync()); + + await (tx.CommitAsync()); + } + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "query first execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "cache misses first execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "cache hits first execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "cache puts first execution"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .ToListAsync()); + + await (tx.CommitAsync()); + } + + Assert.That(items, Has.Count.EqualTo(36)); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "query second execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "cache misses second execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "cache hits second execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "cache puts second execution"); + } + + [TestCase(false)] + [TestCase(true)] + public async Task QueryFetchCollectionBatchCacheTestAsync(bool future) + { + if (future && !Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + { + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + } + + int middleId; + + using (var s = OpenSession()) + { + var ids = await (s.Query().Select(o => o.Id).OrderBy(o => o).ToListAsync()); + middleId = ids[2]; + } + + Sfi.Statistics.Clear(); + + List items; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .ToListAsync()); + } + + await (tx.CommitAsync()); + } + + Assert.That(items, Has.Count.EqualTo(future ? 3 : 6), "Unexpected items count"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .ToListAsync()); + } + + await (tx.CommitAsync()); + } + + Assert.That(items, Has.Count.EqualTo(future ? 3 : 6)); + 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(future ? 2 : 1), "Unexpected cache hit count"); + } + + [TestCase(false)] + [TestCase(true)] + public async Task QueryFetchEntityBatchCacheTestAsync(bool future) + { + if (future && !Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + { + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + } + + int middleId; + + using (var s = OpenSession()) + { + var ids = await (s.Query().Select(o => o.Id).OrderBy(o => o).ToListAsync()); + middleId = ids[17]; + } + + Sfi.Statistics.Clear(); + + List items; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .ToListAsync()); + } + + await (tx.CommitAsync()); + } + + Assert.That(items, Has.Count.EqualTo(future ? 18 : 36), "Unexpected items count"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = await (s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .ToListAsync()); + } + + await (tx.CommitAsync()); + } + + Assert.That(items, Has.Count.EqualTo(future ? 18 : 36)); + 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(future ? 2 : 1), "Unexpected cache hit count"); + } + } +} diff --git a/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs b/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs index ff81250d911..848ead9056e 100644 --- a/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs +++ b/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs @@ -139,7 +139,7 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() await (CreateItemsAsync()); - await (DoMutiQueryAndAssertAsync()); + await (DoMultiQueryAndAssertAsync()); Assert.AreEqual(1, cacheHashtable.Count); } @@ -148,19 +148,20 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() public async Task CanGetMultiQueryFromSecondLevelCacheAsync() { await (CreateItemsAsync()); - //set the query in the cache - await (DoMutiQueryAndAssertAsync()); + // Set the query in the cache. + await (DoMultiQueryAndAssertAsync()); var cacheHashtable = MultipleQueriesFixtureAsync.GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2; using (var s = Sfi.OpenSession()) @@ -172,9 +173,9 @@ public async Task CanGetMultiQueryFromSecondLevelCacheAsync() .Add(CriteriaTransformer.Clone(criteria).SetProjection(Projections.RowCount())); multiCriteria.SetCacheable(true); var results = await (multiCriteria.ListAsync()); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (int)((IList)results[1])[0]; + var count = (int) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -184,12 +185,12 @@ public async Task CanUpdateStatisticsWhenGetMultiQueryFromSecondLevelCacheAsync( { await (CreateItemsAsync()); - await (DoMutiQueryAndAssertAsync()); + await (DoMultiQueryAndAssertAsync()); Assert.AreEqual(0, Sfi.Statistics.QueryCacheHitCount); Assert.AreEqual(1, Sfi.Statistics.QueryCacheMissCount); Assert.AreEqual(1, Sfi.Statistics.QueryCachePutCount); - await (DoMutiQueryAndAssertAsync()); + await (DoMultiQueryAndAssertAsync()); Assert.AreEqual(1, Sfi.Statistics.QueryCacheHitCount); Assert.AreEqual(1, Sfi.Statistics.QueryCacheMissCount); Assert.AreEqual(1, Sfi.Statistics.QueryCachePutCount); @@ -390,7 +391,7 @@ public async Task CanNotRetrieveDetachedCriteriaResultWithUnknownKeyAsync() } } - private async Task DoMutiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) + private async Task DoMultiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var s = OpenSession()) { @@ -401,9 +402,9 @@ public async Task CanNotRetrieveDetachedCriteriaResultWithUnknownKeyAsync() .Add(CriteriaTransformer.Clone(criteria).SetProjection(Projections.RowCount())); multiCriteria.SetCacheable(true); var results = await (multiCriteria.ListAsync(cancellationToken)); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (int)((IList)results[1])[0]; + var count = (int) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/Async/QueryTest/MultipleMixedQueriesFixture.cs b/src/NHibernate.Test/Async/QueryTest/MultipleMixedQueriesFixture.cs index 5fc7d5f721a..9e4b39d77cf 100644 --- a/src/NHibernate.Test/Async/QueryTest/MultipleMixedQueriesFixture.cs +++ b/src/NHibernate.Test/Async/QueryTest/MultipleMixedQueriesFixture.cs @@ -78,19 +78,20 @@ public void NH_1085_WillGiveReasonableErrorIfBadParameterNameAsync() public async Task CanGetMultiQueryFromSecondLevelCacheAsync() { await (CreateItemsAsync()); - //set the query in the cache - await (DoMutiQueryAndAssertAsync()); + // Set the query in the cache. + await (DoMultiQueryAndAssertAsync()); var cacheHashtable = MultipleQueriesFixtureAsync.GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2L; using (var s = Sfi.OpenSession()) @@ -103,9 +104,9 @@ public async Task CanGetMultiQueryFromSecondLevelCacheAsync() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = await (multiQuery.ListAsync()); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -185,12 +186,12 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() await (CreateItemsAsync()); - await (DoMutiQueryAndAssertAsync()); + await (DoMultiQueryAndAssertAsync()); Assert.AreEqual(1, cacheHashtable.Count); } - private async Task DoMutiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) + private async Task DoMultiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var s = OpenSession()) { @@ -202,9 +203,9 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = await (multiQuery.ListAsync(cancellationToken)); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/Async/QueryTest/MultipleQueriesFixture.cs b/src/NHibernate.Test/Async/QueryTest/MultipleQueriesFixture.cs index 688bce5dc72..e8a84ffbe1a 100644 --- a/src/NHibernate.Test/Async/QueryTest/MultipleQueriesFixture.cs +++ b/src/NHibernate.Test/Async/QueryTest/MultipleQueriesFixture.cs @@ -81,19 +81,20 @@ public void NH_1085_WillGiveReasonableErrorIfBadParameterNameAsync() public async Task CanGetMultiQueryFromSecondLevelCacheAsync() { await (CreateItemsAsync()); - //set the query in the cache - await (DoMutiQueryAndAssertAsync()); + // Set the query in the cache. + await (DoMultiQueryAndAssertAsync()); var cacheHashtable = GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2L; using (var s = Sfi.OpenSession()) @@ -106,9 +107,9 @@ public async Task CanGetMultiQueryFromSecondLevelCacheAsync() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = await (multiQuery.ListAsync()); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -187,12 +188,12 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() await (CreateItemsAsync()); - await (DoMutiQueryAndAssertAsync()); + await (DoMultiQueryAndAssertAsync()); Assert.AreEqual(1, cacheHashtable.Count); } - private async Task DoMutiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) + private async Task DoMultiQueryAndAssertAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var s = OpenSession()) { @@ -204,9 +205,9 @@ public async Task CanUseSecondLevelCacheWithPositionalParametersAsync() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = await (multiQuery.ListAsync(cancellationToken)); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs index 5ef4ebad9d8..6beefe2afa8 100644 --- a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -10,9 +10,10 @@ using System.Collections; using System.Linq; +using NHibernate.Criterion; +using NHibernate.Multi; using NHibernate.Transform; using NUnit.Framework; -using NHibernate.Criterion; namespace NHibernate.Test.SqlTest.Query { @@ -62,6 +63,20 @@ protected override string MappingsAssembly get { return "NHibernate.Test"; } } + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Employment").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + Sfi.QueryCache.Clear(); + } + [Test] public async Task FailOnNoAddEntityOrScalarAsync() { @@ -136,18 +151,6 @@ public async Task SQLQueryInterfaceAsync() await (t.CommitAsync()); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - await (s.DeleteAsync(emp)); - await (s.DeleteAsync(gavin)); - await (s.DeleteAsync(ifa)); - await (s.DeleteAsync(jboss)); - - await (t.CommitAsync()); - s.Close(); - } } [Test] @@ -202,22 +205,10 @@ public async Task SQLQueryInterfaceCacheableAsync() await (t.CommitAsync()); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - await (s.DeleteAsync(emp)); - await (s.DeleteAsync(gavin)); - await (s.DeleteAsync(ifa)); - await (s.DeleteAsync(jboss)); - - await (t.CommitAsync()); - s.Close(); - } } [Test(Description = "GH-2904")] - public async Task CacheableScalarSQLQueryAsync() + public async Task CacheableScalarSqlQueryAsync() { Organization ifa = new Organization("IFA"); Organization jboss = new Organization("JBoss"); @@ -270,25 +261,16 @@ Task GetCacheableSqlQueryResultsAsync() Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "results are expected from cache"); } } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - await (s.DeleteAsync(emp)); - await (s.DeleteAsync(gavin)); - await (s.DeleteAsync(ifa)); - await (s.DeleteAsync(jboss)); - await (t.CommitAsync()); - } } class ResultDto { + public long orgId { get; set; } public string regionCode { get; set; } } [Test(Description = "GH-3169")] - public async Task CacheableScalarSQLQueryWithTransformerAsync() + public async Task CacheableScalarSqlQueryWithTransformerAsync() { Organization ifa = new Organization("IFA"); @@ -312,23 +294,170 @@ async Task AssertQueryAsync(bool fromCache) .ListAsync()); await (t.CommitAsync()); - Assert.AreEqual(1, l.Count); - //TODO: Uncomment if we properly fix caching auto discovery type queries with transformers - // var msg = "results are expected from " + (fromCache ? "cache" : "DB"); - // Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); - // Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + Assert.That(l.Count, Is.EqualTo(1)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); } } await (AssertQueryAsync(false)); await (AssertQueryAsync(true)); + } - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) + [Test(Description = "GH-3169")] + public async Task CacheableScalarSqlEmptyQueryWithTransformerAsync() + { + async Task AssertQueryAsync(bool fromCache) { - await (s.DeleteAsync(ifa)); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = await (s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .ListAsync()); + await (t.CommitAsync()); + + Assert.That(l.Count, Is.EqualTo(0)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); + } + + [Test(Description = "GH-3169")] + public async Task CacheableScalarSqlMultiQueryWithTransformerAsync() + { + Organization ifa = new Organization("IFA"); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.SaveAsync(ifa)); await (t.CommitAsync()); } + + async Task AssertQueryAsync(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + await (batch.ExecuteAsync()); + + var l1 = await (batch.GetResultAsync(0)); + var l2 = await (batch.GetResultAsync(1)); + + await (t.CommitAsync()); + + Assert.That(l1.Count, Is.EqualTo(1), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(1), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); + } + + [Test(Description = "GH-3169")] + public async Task CacheableScalarSqlEmptyMultiQueryWithTransformerAsync() + { + async Task AssertQueryAsync(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + await (batch.ExecuteAsync()); + + var l1 = await (batch.GetResultAsync(0)); + var l2 = await (batch.GetResultAsync(1)); + + await (t.CommitAsync()); + + Assert.That(l1.Count, Is.EqualTo(0), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(0), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); + } + + [Test(Description = "GH-3169")] + public async Task CacheableMultiScalarSqlQueryWithTransformerAsync() + { + Organization ifa = new Organization("IFA"); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.SaveAsync(ifa)); + await (t.CommitAsync()); + } + + async Task AssertQueryAsync(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = await (s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .ListAsync()); + await (t.CommitAsync()); + + Assert.That(l.Count, Is.EqualTo(1)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + await (AssertQueryAsync(false)); + await (AssertQueryAsync(true)); } [Test] @@ -356,11 +485,6 @@ public async Task ResultSetMappingDefinitionAsync() .ListAsync()); Assert.AreEqual(l.Count, 1); - await (s.DeleteAsync(emp)); - await (s.DeleteAsync(gavin)); - await (s.DeleteAsync(ifa)); - await (s.DeleteAsync(jboss)); - await (t.CommitAsync()); s.Close(); } @@ -443,8 +567,6 @@ public async Task ScalarValuesAsync() Assert.AreEqual(o[1], "JBoss"); Assert.AreEqual(o[0], idJBoss); - await (s.DeleteAsync(ifa)); - await (s.DeleteAsync(jboss)); await (t.CommitAsync()); s.Close(); } diff --git a/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs b/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs new file mode 100644 index 00000000000..bf1009c90d3 --- /dev/null +++ b/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs @@ -0,0 +1,525 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Caches.CoreDistributedCache; +using NHibernate.Caches.CoreDistributedCache.Memory; +using NHibernate.Caches.Util.JsonSerializer; +using NHibernate.Cfg; +using NHibernate.Linq; +using NHibernate.Multi; +using NHibernate.Transform; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.CacheTest +{ + [TestFixture] + public class JsonSerializerCacheFixture : TestCase + { + protected override string[] Mappings => new[] + { + "CacheTest.ReadOnly.hbm.xml", + "CacheTest.ReadWrite.hbm.xml" + }; + + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.UseSecondLevelCache, "true"); + configuration.SetProperty(Environment.UseQueryCache, "true"); + configuration.SetProperty(Environment.GenerateStatistics, "true"); + configuration.SetProperty(Environment.CacheProvider, typeof(CoreDistributedCacheProvider).AssemblyQualifiedName); + CoreDistributedCacheProvider.CacheFactory = new MemoryFactory(); + var serializer = new JsonCacheSerializer(); + serializer.RegisterType(typeof(Tuple), "tso"); + CoreDistributedCacheProvider.DefaultSerializer = serializer; + } + + protected override void OnSetUp() + { + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + var totalItems = 6; + for (var i = 1; i <= totalItems; i++) + { + var parent = new ReadOnly + { + Name = $"Name{i}" + }; + for (var j = 1; j <= totalItems; j++) + { + var child = new ReadOnlyItem + { + Parent = parent + }; + parent.Items.Add(child); + } + s.Save(parent); + } + for (var i = 1; i <= totalItems; i++) + { + var parent = new ReadWrite + { + Name = $"Name{i}" + }; + for (var j = 1; j <= totalItems; j++) + { + var child = new ReadWriteItem + { + Parent = parent + }; + parent.Items.Add(child); + } + s.Save(parent); + } + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from ReadOnlyItem").ExecuteUpdate(); + s.CreateQuery("delete from ReadWriteItem").ExecuteUpdate(); + s.CreateQuery("delete from ReadOnly").ExecuteUpdate(); + s.CreateQuery("delete from ReadWrite").ExecuteUpdate(); + tx.Commit(); + } + // Must rebuild the session factory, CoreDistribted cache being not clearable. + RebuildSessionFactory(); + } + + [Test] + public void CacheableScalarSqlQueryWithTransformer() + { + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = s.CreateSQLQuery("select ro.Name as RegionCode from ReadOnly ro") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .List(); + t.Commit(); + + Assert.That(l.Count, Is.EqualTo(6)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); + } + + [Test] + public void CacheableScalarSqlMultiQueryWithTransformer() + { + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select rw.Name as RegionCode from ReadWrite rw") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select rw.Id as OrgId from ReadWrite rw") + .AddScalar("orgId", NHibernateUtil.Int64) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + batch.Execute(); + + var l1 = batch.GetResult(0); + var l2 = batch.GetResult(1); + + t.Commit(); + + Assert.That(l1.Count, Is.EqualTo(6), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(6), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); + } + + class ResultDto + { + public long OrgId { get; set; } + public string RegionCode { get; set; } + } + + [Test] + public void QueryCacheTest() + { + // QueryCache batching is used by QueryBatch. + if (!Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + + Sfi.Statistics.Clear(); + + using var s = OpenSession(); + + const string query = "from ReadOnly e where e.Name = :name"; + const string name1 = "Name1"; + const string name2 = "Name2"; + const string name3 = "Name3"; + const string name4 = "Name4"; + const string name5 = "Name5"; + var q1 = + s + .CreateQuery(query) + .SetString("name", name1) + .SetCacheable(true); + var q2 = + s + .CreateQuery(query) + .SetString("name", name2) + .SetCacheable(true); + var q3 = + s + .Query() + .Where(r => r.Name == name3) + .WithOptions(o => o.SetCacheable(true)); + var q4 = + s + .QueryOver() + .Where(r => r.Name == name4) + .Cacheable(); + var q5 = + s + .CreateSQLQuery("select * " + query) + .AddEntity(typeof(ReadOnly)) + .SetString("name", name5) + .SetCacheable(true); + + var queries = + s + .CreateQueryBatch() + .Add(q1) + .Add(q2) + .Add(q3) + .Add(q4) + .Add(q5); + + using (var t = s.BeginTransaction()) + { + queries.Execute(); + t.Commit(); + } + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "queries first execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(5), "cache misses first execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "cache hits first execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "cache puts first execution"); + + // Run a second time, to test the query cache + using (var t = s.BeginTransaction()) + { + queries.Execute(); + t.Commit(); + } + + Assert.That( + queries.GetResult(0), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1"); + Assert.That( + queries.GetResult(1), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2"); + Assert.That( + queries.GetResult(2), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name3), "q3"); + Assert.That( + queries.GetResult(3), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4"); + Assert.That( + queries.GetResult(4), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "queries second execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(5), "cache misses second execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(5), "cache hits second execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "cache puts second execution"); + + // Update an entity to invalidate them + using (var t = s.BeginTransaction()) + { + var readwrite1 = s.Query().Single(e => e.Name == name3); + readwrite1.Name = "NewName"; + t.Commit(); + } + + // Run a third time, to re-test the query cache + using (var t = s.BeginTransaction()) + { + queries.Execute(); + t.Commit(); + } + + Assert.That( + queries.GetResult(0), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1 after update"); + Assert.That( + queries.GetResult(1), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2 after update"); + Assert.That( + queries.GetResult(2), + Has.Count.EqualTo(0), "q3 after update"); + Assert.That( + queries.GetResult(3), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4 after update"); + Assert.That( + queries.GetResult(4), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5 after update"); + + // The two ReadWrite queries should have been re-executed, so count should have been incremented accordingly. + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(3), "queries third execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(7), "cache misses third execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(8), "cache hits third execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(7), "cache puts third execution"); + } + + [Test] + public void QueryEntityBatchCacheTest() + { + Sfi.Statistics.Clear(); + + List items; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .ToList(); + + tx.Commit(); + } + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "query first execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "cache misses first execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "cache hits first execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "cache puts first execution"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .ToList(); + + tx.Commit(); + } + + Assert.That(items, Has.Count.EqualTo(36)); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "query second execution count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "cache misses second execution"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "cache hits second execution"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "cache puts second execution"); + } + + [TestCase(false)] + [TestCase(true)] + public void QueryFetchCollectionBatchCacheTest(bool future) + { + if (future && !Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + { + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + } + + int middleId; + + using (var s = OpenSession()) + { + var ids = s.Query().Select(o => o.Id).OrderBy(o => o).ToList(); + middleId = ids[2]; + } + + Sfi.Statistics.Clear(); + + List items; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .ToList(); + } + + tx.Commit(); + } + + Assert.That(items, Has.Count.EqualTo(future ? 3 : 6), "Unexpected items count"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .FetchMany(o => o.Items) + .ToList(); + } + + tx.Commit(); + } + + Assert.That(items, Has.Count.EqualTo(future ? 3 : 6)); + 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(future ? 2 : 1), "Unexpected cache hit count"); + } + + [TestCase(false)] + [TestCase(true)] + public void QueryFetchEntityBatchCacheTest(bool future) + { + if (future && !Sfi.ConnectionProvider.Driver.SupportsMultipleQueries) + { + Assert.Ignore($"{Sfi.ConnectionProvider.Driver} does not support multiple queries"); + } + + int middleId; + + using (var s = OpenSession()) + { + var ids = s.Query().Select(o => o.Id).OrderBy(o => o).ToList(); + middleId = ids[17]; + } + + Sfi.Statistics.Clear(); + + List items; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .ToList(); + } + + tx.Commit(); + } + + Assert.That(items, Has.Count.EqualTo(future ? 18 : 36), "Unexpected items count"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + if (future) + { + s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id > middleId) + .ToFuture(); + + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .Where(o => o.Id <= middleId) + .ToFuture() + .ToList(); + } + else + { + items = s.Query() + .WithOptions(o => o.SetCacheable(true)) + .Fetch(o => o.Parent) + .ToList(); + } + + tx.Commit(); + } + + Assert.That(items, Has.Count.EqualTo(future ? 18 : 36)); + 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(future ? 2 : 1), "Unexpected cache hit count"); + } + } +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index eb38a252a3a..c32dfd7d6e2 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -57,6 +57,8 @@ + + diff --git a/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs b/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs index 2b170b95905..acebb19a929 100644 --- a/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs +++ b/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs @@ -127,7 +127,7 @@ public void CanUseSecondLevelCacheWithPositionalParameters() CreateItems(); - DoMutiQueryAndAssert(); + DoMultiQueryAndAssert(); Assert.AreEqual(1, cacheHashtable.Count); } @@ -136,19 +136,20 @@ public void CanUseSecondLevelCacheWithPositionalParameters() public void CanGetMultiQueryFromSecondLevelCache() { CreateItems(); - //set the query in the cache - DoMutiQueryAndAssert(); + // Set the query in the cache. + DoMultiQueryAndAssert(); var cacheHashtable = MultipleQueriesFixture.GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2; using (var s = Sfi.OpenSession()) @@ -160,9 +161,9 @@ public void CanGetMultiQueryFromSecondLevelCache() .Add(CriteriaTransformer.Clone(criteria).SetProjection(Projections.RowCount())); multiCriteria.SetCacheable(true); var results = multiCriteria.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (int)((IList)results[1])[0]; + var count = (int) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -172,12 +173,12 @@ public void CanUpdateStatisticsWhenGetMultiQueryFromSecondLevelCache() { CreateItems(); - DoMutiQueryAndAssert(); + DoMultiQueryAndAssert(); Assert.AreEqual(0, Sfi.Statistics.QueryCacheHitCount); Assert.AreEqual(1, Sfi.Statistics.QueryCacheMissCount); Assert.AreEqual(1, Sfi.Statistics.QueryCachePutCount); - DoMutiQueryAndAssert(); + DoMultiQueryAndAssert(); Assert.AreEqual(1, Sfi.Statistics.QueryCacheHitCount); Assert.AreEqual(1, Sfi.Statistics.QueryCacheMissCount); Assert.AreEqual(1, Sfi.Statistics.QueryCachePutCount); @@ -436,7 +437,7 @@ public void CanNotRetrieveDetachedCriteriaResultWithUnknownKey() } } - private void DoMutiQueryAndAssert() + private void DoMultiQueryAndAssert() { using (var s = OpenSession()) { @@ -447,9 +448,9 @@ private void DoMutiQueryAndAssert() .Add(CriteriaTransformer.Clone(criteria).SetProjection(Projections.RowCount())); multiCriteria.SetCacheable(true); var results = multiCriteria.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (int)((IList)results[1])[0]; + var count = (int) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/QueryTest/MultipleMixedQueriesFixture.cs b/src/NHibernate.Test/QueryTest/MultipleMixedQueriesFixture.cs index 561d679a0a7..cfd26a9efac 100644 --- a/src/NHibernate.Test/QueryTest/MultipleMixedQueriesFixture.cs +++ b/src/NHibernate.Test/QueryTest/MultipleMixedQueriesFixture.cs @@ -66,19 +66,20 @@ public void NH_1085_WillGiveReasonableErrorIfBadParameterName() public void CanGetMultiQueryFromSecondLevelCache() { CreateItems(); - //set the query in the cache - DoMutiQueryAndAssert(); + // Set the query in the cache. + DoMultiQueryAndAssert(); var cacheHashtable = MultipleQueriesFixture.GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2L; using (var s = Sfi.OpenSession()) @@ -91,9 +92,9 @@ public void CanGetMultiQueryFromSecondLevelCache() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = multiQuery.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -173,12 +174,12 @@ public void CanUseSecondLevelCacheWithPositionalParameters() CreateItems(); - DoMutiQueryAndAssert(); + DoMultiQueryAndAssert(); Assert.AreEqual(1, cacheHashtable.Count); } - private void DoMutiQueryAndAssert() + private void DoMultiQueryAndAssert() { using (var s = OpenSession()) { @@ -190,9 +191,9 @@ private void DoMutiQueryAndAssert() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = multiQuery.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/QueryTest/MultipleQueriesFixture.cs b/src/NHibernate.Test/QueryTest/MultipleQueriesFixture.cs index 6c4bd1063b6..a4eb5e81b41 100644 --- a/src/NHibernate.Test/QueryTest/MultipleQueriesFixture.cs +++ b/src/NHibernate.Test/QueryTest/MultipleQueriesFixture.cs @@ -69,19 +69,20 @@ public void NH_1085_WillGiveReasonableErrorIfBadParameterName() public void CanGetMultiQueryFromSecondLevelCache() { CreateItems(); - //set the query in the cache - DoMutiQueryAndAssert(); + // Set the query in the cache. + DoMultiQueryAndAssert(); var cacheHashtable = GetHashTableUsedAsQueryCache(Sfi); - var cachedListEntry = (IList)new ArrayList(cacheHashtable.Values)[0]; - var cachedQuery = (IList)cachedListEntry[1]; + var cachedListEntry = (IList) new ArrayList(cacheHashtable.Values)[0]; + // The first element is a timestamp, then only we have the cached data. + var cachedQuery = (IList) cachedListEntry[1] ?? throw new InvalidOperationException("Cached data is null"); - var firstQueryResults = (IList)cachedQuery[0]; + var firstQueryResults = (IList) cachedQuery[0]; firstQueryResults.Clear(); firstQueryResults.Add(3); firstQueryResults.Add(4); - var secondQueryResults = (IList)cachedQuery[1]; + var secondQueryResults = (IList) cachedQuery[1]; secondQueryResults[0] = 2L; using (var s = Sfi.OpenSession()) @@ -94,9 +95,9 @@ public void CanGetMultiQueryFromSecondLevelCache() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = multiQuery.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(2, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(2L, count); } } @@ -175,12 +176,12 @@ public void CanUseSecondLevelCacheWithPositionalParameters() CreateItems(); - DoMutiQueryAndAssert(); + DoMultiQueryAndAssert(); Assert.AreEqual(1, cacheHashtable.Count); } - private void DoMutiQueryAndAssert() + private void DoMultiQueryAndAssert() { using (var s = OpenSession()) { @@ -192,9 +193,9 @@ private void DoMutiQueryAndAssert() .SetInt32(0, 50)); multiQuery.SetCacheable(true); var results = multiQuery.List(); - var items = (IList)results[0]; + var items = (IList) results[0]; Assert.AreEqual(89, items.Count); - var count = (long)((IList)results[1])[0]; + var count = (long) ((IList) results[1])[0]; Assert.AreEqual(99L, count); } } diff --git a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs index 80eeb3c4c73..38c395298df 100644 --- a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -1,8 +1,9 @@ using System.Collections; using System.Linq; +using NHibernate.Criterion; +using NHibernate.Multi; using NHibernate.Transform; using NUnit.Framework; -using NHibernate.Criterion; namespace NHibernate.Test.SqlTest.Query { @@ -51,6 +52,20 @@ protected override string MappingsAssembly get { return "NHibernate.Test"; } } + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Employment").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + Sfi.QueryCache.Clear(); + } + [Test] public void FailOnNoAddEntityOrScalar() { @@ -125,18 +140,6 @@ public void SQLQueryInterface() t.Commit(); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - - t.Commit(); - s.Close(); - } } [Test] @@ -191,22 +194,10 @@ public void SQLQueryInterfaceCacheable() t.Commit(); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - - t.Commit(); - s.Close(); - } } [Test(Description = "GH-2904")] - public void CacheableScalarSQLQuery() + public void CacheableScalarSqlQuery() { Organization ifa = new Organization("IFA"); Organization jboss = new Organization("JBoss"); @@ -252,25 +243,16 @@ IList GetCacheableSqlQueryResults() Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "results are expected from cache"); } } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - t.Commit(); - } } class ResultDto { + public long orgId { get; set; } public string regionCode { get; set; } } [Test(Description = "GH-3169")] - public void CacheableScalarSQLQueryWithTransformer() + public void CacheableScalarSqlQueryWithTransformer() { Organization ifa = new Organization("IFA"); @@ -294,23 +276,170 @@ void AssertQuery(bool fromCache) .List(); t.Commit(); - Assert.AreEqual(1, l.Count); - //TODO: Uncomment if we properly fix caching auto discovery type queries with transformers - // var msg = "results are expected from " + (fromCache ? "cache" : "DB"); - // Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); - // Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + Assert.That(l.Count, Is.EqualTo(1)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); } } AssertQuery(false); AssertQuery(true); + } - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) + [Test(Description = "GH-3169")] + public void CacheableScalarSqlEmptyQueryWithTransformer() + { + void AssertQuery(bool fromCache) { - s.Delete(ifa); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .List(); + t.Commit(); + + Assert.That(l.Count, Is.EqualTo(0)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); + } + + [Test(Description = "GH-3169")] + public void CacheableScalarSqlMultiQueryWithTransformer() + { + Organization ifa = new Organization("IFA"); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.Save(ifa); t.Commit(); } + + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + batch.Execute(); + + var l1 = batch.GetResult(0); + var l2 = batch.GetResult(1); + + t.Commit(); + + Assert.That(l1.Count, Is.EqualTo(1), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(1), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); + } + + [Test(Description = "GH-3169")] + public void CacheableScalarSqlEmptyMultiQueryWithTransformer() + { + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + batch.Execute(); + + var l1 = batch.GetResult(0); + var l2 = batch.GetResult(1); + + t.Commit(); + + Assert.That(l1.Count, Is.EqualTo(0), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(0), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); + } + + [Test(Description = "GH-3169")] + public void CacheableMultiScalarSqlQueryWithTransformer() + { + Organization ifa = new Organization("IFA"); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.Save(ifa); + t.Commit(); + } + + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var l = s.CreateSQLQuery("select org.ORGID as orgId, org.NAME as regionCode from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true) + .List(); + t.Commit(); + + Assert.That(l.Count, Is.EqualTo(1)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); } [Test] @@ -338,11 +467,6 @@ public void ResultSetMappingDefinition() .List(); Assert.AreEqual(l.Count, 1); - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - t.Commit(); s.Close(); } @@ -425,8 +549,6 @@ public void ScalarValues() Assert.AreEqual(o[1], "JBoss"); Assert.AreEqual(o[0], idJBoss); - s.Delete(ifa); - s.Delete(jboss); t.Commit(); s.Close(); } diff --git a/src/NHibernate/Async/Cache/StandardQueryCache.cs b/src/NHibernate/Async/Cache/StandardQueryCache.cs index ab7d4f4547e..d1a0ea7010b 100644 --- a/src/NHibernate/Async/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Async/Cache/StandardQueryCache.cs @@ -13,7 +13,6 @@ using System.Collections.Generic; using System.Linq; using NHibernate.Cfg; -using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -67,7 +66,7 @@ public async Task PutAsync(QueryKey key, ICacheAssembler[] returnTypes, IL Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); - await (Cache.PutAsync(key, await (GetCacheableResultAsync(returnTypes, session, result, ts, cancellationToken)).ConfigureAwait(false), cancellationToken)).ConfigureAwait(false); + await (Cache.PutAsync(key, await (GetCacheableResultAsync(returnTypes, session, result, ts, GetAutoDiscoveredAliases(key), cancellationToken)).ConfigureAwait(false), cancellationToken)).ConfigureAwait(false); return true; } @@ -91,10 +90,35 @@ public async Task GetAsync( try { - // 6.0 TODO: inline the call. -#pragma warning disable 612 - return await (GetAsync(key, returnTypes, queryParameters.NaturalKeyLookup, spaces, session, cancellationToken)).ConfigureAwait(false); -#pragma warning restore 612 + if (Log.IsDebugEnabled()) + Log.Debug("checking cached query results in region: '{0}'; {1}", _regionName, key); + + var cacheable = (IList) await (Cache.GetAsync(key, cancellationToken)).ConfigureAwait(false); + if (cacheable == null) + { + Log.Debug("query results were not found in cache: {0}", key); + return null; + } + + var timestamp = GetResultsMetadata(cacheable, out var aliases); + + if (Log.IsDebugEnabled()) + Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(spaces)); + + if (!queryParameters.NaturalKeyLookup && !await (IsUpToDateAsync(spaces, timestamp, cancellationToken)).ConfigureAwait(false)) + { + Log.Debug("cached query results were not up to date for: {0}", key); + return null; + } + + var result = await (GetResultFromCacheableAsync(key, returnTypes, queryParameters.NaturalKeyLookup, session, cacheable, cancellationToken)).ConfigureAwait(false); + + if (result != null && key.ResultTransformer?.AutoDiscoverTypes == true && result.Count > 0) + { + key.ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters.ResultTransformer, aliases); + } + + return result; } finally { @@ -117,7 +141,7 @@ public async Task GetAsync(QueryKey key, ICacheAssembler[] returnTypes, b return null; } - var timestamp = (long) cacheable[0]; + var timestamp = GetResultsMetadata(cacheable, out _); if (Log.IsDebugEnabled()) Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(spaces)); @@ -153,9 +177,10 @@ public async Task PutManyAsync( if (queryParameters[i].NaturalKeyLookup && result.Count == 0) continue; + var key = keys[i]; cached[i] = true; - cachedKeys.Add(keys[i]); - cachedResults.Add(await (GetCacheableResultAsync(returnTypes[i], session, result, ts, cancellationToken)).ConfigureAwait(false)); + cachedKeys.Add(key); + cachedResults.Add(await (GetCacheableResultAsync(returnTypes[i], session, result, ts, GetAutoDiscoveredAliases(key), cancellationToken)).ConfigureAwait(false)); } await (_cache.PutManyAsync(cachedKeys.ToArray(), cachedResults.ToArray(), cancellationToken)).ConfigureAwait(false); @@ -189,14 +214,20 @@ public async Task GetManyAsync( continue; } + var timestamp = GetResultsMetadata(cacheable, out var aliases); + var key = keys[i]; + if (key.ResultTransformer?.AutoDiscoverTypes == true && !IsEmpty(cacheable)) + { + key.ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters[i].ResultTransformer, aliases); + } + var querySpaces = spaces[i]; if (queryParameters[i].NaturalKeyLookup || querySpaces.Count == 0) continue; spacesToCheck.Add(querySpaces); checkedSpacesIndexes.Add(i); - // The timestamp is the first element of the cache result. - checkedSpacesTimestamp.Add((long) cacheable[0]); + checkedSpacesTimestamp.Add(timestamp); if (Log.IsDebugEnabled()) Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(querySpaces)); } @@ -224,6 +255,7 @@ public async Task GetManyAsync( if (checkedSpacesIndexes.Contains(i) && !upToDates[upToDatesIndex++]) { Log.Debug("cached query results were not up to date for: {0}", key); + cacheables[i] = null; continue; } @@ -241,7 +273,7 @@ public async Task GetManyAsync( for (var i = 0; i < keys.Length; i++) { - if (finalReturnTypes[i] == null) + if (cacheables[i] == null) { continue; } @@ -264,7 +296,7 @@ public async Task GetManyAsync( for (var i = 0; i < keys.Length; i++) { - if (finalReturnTypes[i] == null) + if (cacheables[i] == null) { continue; } @@ -299,10 +331,16 @@ private static async Task> GetCacheableResultAsync( ICacheAssembler[] returnTypes, ISessionImplementor session, IList result, - long ts, CancellationToken cancellationToken) + long ts, + string[] aliases, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var cacheable = new List(result.Count + 1) { ts }; + var cacheable = + new List(result.Count + 1) + { + aliases == null ? ts : new object[] { ts, aliases.ToArray() } + }; + foreach (var row in result) { if (returnTypes.Length == 1) @@ -311,7 +349,7 @@ private static async Task> GetCacheableResultAsync( } else { - cacheable.Add(await (TypeHelper.DisassembleAsync((object[])row, returnTypes, null, session, null, cancellationToken)).ConfigureAwait(false)); + cacheable.Add(await (TypeHelper.DisassembleAsync((object[]) row, returnTypes, null, session, null, cancellationToken)).ConfigureAwait(false)); } } @@ -324,22 +362,23 @@ private static async Task PerformBeforeAssembleAsync( IList cacheable, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + if (IsEmpty(cacheable)) + return; + if (returnTypes.Length == 1) { var returnType = returnTypes[0]; - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - await (returnType.BeforeAssembleAsync(cacheable[i], session, cancellationToken)).ConfigureAwait(false); + await (returnType.BeforeAssembleAsync(cached, session, cancellationToken)).ConfigureAwait(false); } } else { - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - await (TypeHelper.BeforeAssembleAsync((object[]) cacheable[i], returnTypes, session, cancellationToken)).ConfigureAwait(false); + await (TypeHelper.BeforeAssembleAsync((object[]) cached, returnTypes, session, cancellationToken)).ConfigureAwait(false); } } } @@ -355,14 +394,16 @@ private async Task PerformAssembleAsync( try { var result = new List(cacheable.Count - 1); + if (IsEmpty(cacheable)) + return result; + if (returnTypes.Length == 1) { var returnType = returnTypes[0]; - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - result.Add(await (returnType.AssembleAsync(cacheable[i], session, null, cancellationToken)).ConfigureAwait(false)); + result.Add(await (returnType.AssembleAsync(cached, session, null, cancellationToken)).ConfigureAwait(false)); } } else @@ -376,10 +417,9 @@ private async Task PerformAssembleAsync( } } - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - result.Add(await (TypeHelper.AssembleAsync((object[]) cacheable[i], returnTypes, nonCollectionTypeIndexes, session, cancellationToken)).ConfigureAwait(false)); + result.Add(await (TypeHelper.AssembleAsync((object[]) cached, returnTypes, nonCollectionTypeIndexes, session, cancellationToken)).ConfigureAwait(false)); } } @@ -411,6 +451,9 @@ private static async Task InitializeCollectionsAsync( IList cacheResult, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + if (IsEmpty(cacheResult)) + return; + var collectionIndexes = new Dictionary(); for (var i = 0; i < returnTypes.Length; i++) { @@ -425,17 +468,18 @@ private static async Task InitializeCollectionsAsync( return; } - // Skip first element, it is the timestamp - for (var i = 1; i < cacheResult.Count; i++) + var j = 0; + foreach (var cached in GetResultsEnumerable(cacheResult)) { // 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 when their owners were assembled so we only // have to initialize them. await (TypeHelper.InitializeCollectionsAsync( - (object[]) cacheResult[i], - (object[]) assembleResult[i - 1], + (object[]) cached, + (object[]) assembleResult[j], collectionIndexes, session, cancellationToken)).ConfigureAwait(false); + j++; } } @@ -448,6 +492,10 @@ private async Task GetResultFromCacheableAsync( { cancellationToken.ThrowIfCancellationRequested(); Log.Debug("returning cached query results for: {0}", key); + + if (IsEmpty(cacheable)) + return new List(); + returnTypes = GetReturnTypes(key, returnTypes, cacheable); try { diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index 8cda99f3815..2367a0fa2f5 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using NHibernate.Cfg; -using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -19,7 +18,7 @@ namespace NHibernate.Cache /// public partial class StandardQueryCache : IQueryCache, IBatchableQueryCache { - private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof (StandardQueryCache)); + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(StandardQueryCache)); private readonly string _regionName; private readonly UpdateTimestampsCache _updateTimestampsCache; private readonly CacheBase _cache; @@ -101,7 +100,7 @@ public bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); - Cache.Put(key, GetCacheableResult(returnTypes, session, result, ts)); + Cache.Put(key, GetCacheableResult(returnTypes, session, result, ts, GetAutoDiscoveredAliases(key))); return true; } @@ -124,10 +123,35 @@ public IList Get( try { - // 6.0 TODO: inline the call. -#pragma warning disable 612 - return Get(key, returnTypes, queryParameters.NaturalKeyLookup, spaces, session); -#pragma warning restore 612 + if (Log.IsDebugEnabled()) + Log.Debug("checking cached query results in region: '{0}'; {1}", _regionName, key); + + var cacheable = (IList) Cache.Get(key); + if (cacheable == null) + { + Log.Debug("query results were not found in cache: {0}", key); + return null; + } + + var timestamp = GetResultsMetadata(cacheable, out var aliases); + + if (Log.IsDebugEnabled()) + Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(spaces)); + + if (!queryParameters.NaturalKeyLookup && !IsUpToDate(spaces, timestamp)) + { + Log.Debug("cached query results were not up to date for: {0}", key); + return null; + } + + var result = GetResultFromCacheable(key, returnTypes, queryParameters.NaturalKeyLookup, session, cacheable); + + if (result != null && key.ResultTransformer?.AutoDiscoverTypes == true && result.Count > 0) + { + key.ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters.ResultTransformer, aliases); + } + + return result; } finally { @@ -149,7 +173,7 @@ public IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyL return null; } - var timestamp = (long) cacheable[0]; + var timestamp = GetResultsMetadata(cacheable, out _); if (Log.IsDebugEnabled()) Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(spaces)); @@ -184,9 +208,10 @@ public bool[] PutMany( if (queryParameters[i].NaturalKeyLookup && result.Count == 0) continue; + var key = keys[i]; cached[i] = true; - cachedKeys.Add(keys[i]); - cachedResults.Add(GetCacheableResult(returnTypes[i], session, result, ts)); + cachedKeys.Add(key); + cachedResults.Add(GetCacheableResult(returnTypes[i], session, result, ts, GetAutoDiscoveredAliases(key))); } _cache.PutMany(cachedKeys.ToArray(), cachedResults.ToArray()); @@ -194,6 +219,11 @@ public bool[] PutMany( return cached; } + private static string[] GetAutoDiscoveredAliases(QueryKey key) + { + return key.ResultTransformer?.AutoDiscoveredAliases; + } + /// public IList[] GetMany( QueryKey[] keys, @@ -219,14 +249,20 @@ public IList[] GetMany( continue; } + var timestamp = GetResultsMetadata(cacheable, out var aliases); + var key = keys[i]; + if (key.ResultTransformer?.AutoDiscoverTypes == true && !IsEmpty(cacheable)) + { + key.ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters[i].ResultTransformer, aliases); + } + var querySpaces = spaces[i]; if (queryParameters[i].NaturalKeyLookup || querySpaces.Count == 0) continue; spacesToCheck.Add(querySpaces); checkedSpacesIndexes.Add(i); - // The timestamp is the first element of the cache result. - checkedSpacesTimestamp.Add((long) cacheable[0]); + checkedSpacesTimestamp.Add(timestamp); if (Log.IsDebugEnabled()) Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(querySpaces)); } @@ -254,6 +290,7 @@ public IList[] GetMany( if (checkedSpacesIndexes.Contains(i) && !upToDates[upToDatesIndex++]) { Log.Debug("cached query results were not up to date for: {0}", key); + cacheables[i] = null; continue; } @@ -271,7 +308,7 @@ public IList[] GetMany( for (var i = 0; i < keys.Length; i++) { - if (finalReturnTypes[i] == null) + if (cacheables[i] == null) { continue; } @@ -294,7 +331,7 @@ public IList[] GetMany( for (var i = 0; i < keys.Length; i++) { - if (finalReturnTypes[i] == null) + if (cacheables[i] == null) { continue; } @@ -335,9 +372,15 @@ private static List GetCacheableResult( ICacheAssembler[] returnTypes, ISessionImplementor session, IList result, - long ts) + long ts, + string[] aliases) { - var cacheable = new List(result.Count + 1) { ts }; + var cacheable = + new List(result.Count + 1) + { + aliases == null ? ts : new object[] { ts, aliases.ToArray() } + }; + foreach (var row in result) { if (returnTypes.Length == 1) @@ -346,24 +389,51 @@ private static List GetCacheableResult( } else { - cacheable.Add(TypeHelper.Disassemble((object[])row, returnTypes, null, session, null)); + cacheable.Add(TypeHelper.Disassemble((object[]) row, returnTypes, null, session, null)); } } return cacheable; } + private static long GetResultsMetadata(IList cacheable, out string[] aliases) + { + aliases = null; + + var metadata = cacheable[0]; + if (metadata is long timestamp) + return timestamp; + + var metadataArray = (object[]) metadata; + aliases = ((object[]) metadataArray[1]).Cast().ToArray(); + return (long) metadataArray[0]; + } + + private static bool IsEmpty(IList cacheable) + // First element is metadata. + => cacheable.Count <= 1; + + private static IEnumerable GetResultsEnumerable(IList cacheable) + { + // Skip first element, it is the timestamp. + for (var i = 1; i < cacheable.Count; i++) + { + yield return cacheable[i]; + } + } + private static ICacheAssembler[] GetReturnTypes( QueryKey key, ICacheAssembler[] returnTypes, IList cacheable) { - if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) - { + if (IsEmpty(cacheable)) + return returnTypes; + + if (key.ResultTransformer?.AutoDiscoverTypes == true) returnTypes = GuessTypes(cacheable); - } - return returnTypes; + return returnTypes ?? throw new HibernateException("Return types for non empty query results are null, cannot assemble the results"); } private static void PerformBeforeAssemble( @@ -371,22 +441,23 @@ private static void PerformBeforeAssemble( ISessionImplementor session, IList cacheable) { + if (IsEmpty(cacheable)) + return; + if (returnTypes.Length == 1) { var returnType = returnTypes[0]; - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - returnType.BeforeAssemble(cacheable[i], session); + returnType.BeforeAssemble(cached, session); } } else { - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - TypeHelper.BeforeAssemble((object[]) cacheable[i], returnTypes, session); + TypeHelper.BeforeAssemble((object[]) cached, returnTypes, session); } } } @@ -401,14 +472,16 @@ private IList PerformAssemble( try { var result = new List(cacheable.Count - 1); + if (IsEmpty(cacheable)) + return result; + if (returnTypes.Length == 1) { var returnType = returnTypes[0]; - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - result.Add(returnType.Assemble(cacheable[i], session, null)); + result.Add(returnType.Assemble(cached, session, null)); } } else @@ -422,10 +495,9 @@ private IList PerformAssemble( } } - // Skip first element, it is the timestamp - for (var i = 1; i < cacheable.Count; i++) + foreach (var cached in GetResultsEnumerable(cacheable)) { - result.Add(TypeHelper.Assemble((object[]) cacheable[i], returnTypes, nonCollectionTypeIndexes, session)); + result.Add(TypeHelper.Assemble((object[]) cached, returnTypes, nonCollectionTypeIndexes, session)); } } @@ -456,6 +528,9 @@ private static void InitializeCollections( IList assembleResult, IList cacheResult) { + if (IsEmpty(cacheResult)) + return; + var collectionIndexes = new Dictionary(); for (var i = 0; i < returnTypes.Length; i++) { @@ -470,17 +545,18 @@ private static void InitializeCollections( return; } - // Skip first element, it is the timestamp - for (var i = 1; i < cacheResult.Count; i++) + var j = 0; + foreach (var cached in GetResultsEnumerable(cacheResult)) { // 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 when their owners were assembled so we only // have to initialize them. TypeHelper.InitializeCollections( - (object[]) cacheResult[i], - (object[]) assembleResult[i - 1], + (object[]) cached, + (object[]) assembleResult[j], collectionIndexes, session); + j++; } } @@ -492,6 +568,10 @@ private IList GetResultFromCacheable( IList cacheable) { Log.Debug("returning cached query results for: {0}", key); + + if (IsEmpty(cacheable)) + return new List(); + returnTypes = GetReturnTypes(key, returnTypes, cacheable); try { @@ -510,23 +590,24 @@ private IList GetResultFromCacheable( private static ICacheAssembler[] GuessTypes(IList cacheable) { - var colCount = (cacheable[0] as object[])?.Length ?? 1; + var colCount = (GetResultsEnumerable(cacheable).First() as object[])?.Length ?? 1; var returnTypes = new ICacheAssembler[colCount]; if (colCount == 1) { - foreach (var obj in cacheable) + foreach (var cached in GetResultsEnumerable(cacheable)) { - if (obj == null) + if (cached == null) continue; - returnTypes[0] = NHibernateUtil.GuessType(obj); + returnTypes[0] = NHibernateUtil.GuessType(cached); break; } } else { var foundTypes = 0; - foreach (object[] row in cacheable) + foreach (var cached in GetResultsEnumerable(cacheable)) { + var row = (object[]) cached; for (var i = 0; i < colCount; i++) { if (row[i] != null && returnTypes[i] == null) @@ -535,10 +616,12 @@ private static ICacheAssembler[] GuessTypes(IList cacheable) foundTypes++; } } + if (foundTypes == colCount) break; } } + // If a column value was null for all rows, its type is still null: put a type which will just yield null // on null value. for (var i = 0; i < colCount; i++) @@ -546,6 +629,7 @@ private static ICacheAssembler[] GuessTypes(IList cacheable) if (returnTypes[i] == null) returnTypes[i] = NHibernateUtil.String; } + return returnTypes; } diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 5005505c56d..6a74b42032b 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1842,9 +1842,8 @@ internal virtual bool IsCacheable(QueryParameters queryParameters) internal bool IsCacheable(QueryParameters queryParameters, bool supportsQueryCache, IEnumerable persisters) { - bool isCacheable = Factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable - && !(queryParameters.HasAutoDiscoverScalarTypes && queryParameters.ResultTransformer != null); - if (isCacheable && !supportsQueryCache) + bool isCacheable = Factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable; + if (isCacheable && !supportsQueryCache) { if (Factory.Settings.QueryThrowNeverCached) { diff --git a/src/NHibernate/Transform/CacheableResultTransformer.cs b/src/NHibernate/Transform/CacheableResultTransformer.cs index e3dba66d028..5c6b1001639 100644 --- a/src/NHibernate/Transform/CacheableResultTransformer.cs +++ b/src/NHibernate/Transform/CacheableResultTransformer.cs @@ -23,6 +23,11 @@ public class CacheableResultTransformer : IResultTransformer public bool AutoDiscoverTypes { get; } + /// + /// The auto-discovered aliases. + /// + public string[] AutoDiscoveredAliases { get; private set; } + private readonly SqlString _autoDiscoveredQuery; private readonly bool _skipTransformer; private int _tupleLength; @@ -202,6 +207,8 @@ internal void SupplyAutoDiscoveredParameters(IResultTransformer transformer, str throw new InvalidOperationException( "Cannot supply auto-discovered parameters when it is not enabled on the transformer."); + AutoDiscoveredAliases = aliases; + var includeInTuple = ArrayHelper.Fill(true, aliases?.Length ?? 0); InitializeTransformer(includeInTuple, GetIncludeInTransform(transformer, aliases, includeInTuple)); } @@ -225,12 +232,14 @@ public object TransformTuple(object[] tuple, string[] aliases) /// The aliases that correspond to the untransformed tuple. /// The transformer for the re-transformation. /// - /// transformedResults, with each element re-transformed (if necessary). + /// , with each element re-transformed (if necessary). public IList RetransformResults(IList transformedResults, string[] aliases, IResultTransformer transformer, bool[] includeInTuple) { + if (transformedResults.Count == 0) + return transformedResults; if (transformer == null) throw new ArgumentNullException(nameof(transformer)); if (_includeInTuple == null) @@ -280,8 +289,7 @@ public IList RetransformResults(IList transformedResults, /// elements are replaced with untransformed values) and the original /// List is returned. /// - /// If not unnecessary, the original List is returned - /// unchanged. + /// If not necessary, the original List is returned unchanged. /// /// /// @@ -290,9 +298,12 @@ public IList RetransformResults(IList transformedResults, /// excluded tuple elements will be null. /// /// Results that were previously transformed. - /// results, with each element untransformed (if necessary). + /// , with each element untransformed (if necessary). public IList UntransformToTuples(IList results) { + if (results.Count == 0) + return results; + if (_includeInTuple == null) throw new InvalidOperationException("This transformer is not initialized"); if (_includeInTransformIndex == null)