diff --git a/Tools/packages.config b/Tools/packages.config index 367755e5532..438a75071ab 100644 --- a/Tools/packages.config +++ b/Tools/packages.config @@ -7,7 +7,7 @@ - + diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 47966a224ff..f973b07fe72 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -98,6 +98,12 @@ - conversion: Ignore name: QuoteTableAndColumns containingTypeName: SchemaMetadataUpdater + - conversion: Ignore + name: GetEnumerator + containingTypeName: DelayedEnumerator + - conversion: Ignore + name: GetEnumerator + containingTypeName: IFutureEnumerable - conversion: ToAsync name: ExecuteReader containingTypeName: IBatcher diff --git a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs index dbfbb9d6c70..c2baef09b8d 100644 --- a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs @@ -9,22 +9,20 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using NHibernate.Cache; using NHibernate.Cfg; -using NHibernate.DomainModel; +using NHibernate.Linq; +using NHibernate.Multi; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; using Environment = NHibernate.Cfg.Environment; -using NHibernate.Linq; namespace NHibernate.Test.CacheTest { + using System.Threading.Tasks; using System.Threading; [TestFixture] public class BatchableCacheFixtureAsync : TestCase @@ -46,12 +44,6 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); } - protected override bool CheckDatabaseWasCleaned() - { - base.CheckDatabaseWasCleaned(); - return true; // We are unable to delete read-only items. - } - protected override void OnSetUp() { using (var s = Sfi.OpenSession()) @@ -99,9 +91,16 @@ protected override void OnTearDown() using (var s = OpenSession()) using (var tx = s.BeginTransaction()) { - s.Delete("from ReadWrite"); + 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 manually evict "readonly" entities since their caches are readonly + Sfi.Evict(typeof(ReadOnly)); + Sfi.Evict(typeof(ReadOnlyItem)); + Sfi.EvictQueries(); } [Test] @@ -110,7 +109,6 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() var persister = Sfi.GetCollectionPersister($"{typeof(ReadOnly).FullName}.Items"); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -128,7 +126,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() // DefaultInitializeCollectionEventListener and the other time in BatchingCollectionInitializer. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4}, // triggered by InitializeCollectionFromCache method of DefaultInitializeCollectionEventListener type new[] {1, 2, 3, 4, 5}, // triggered by Initialize method of BatchingCollectionInitializer type @@ -140,7 +138,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() // the nearest before the demanded collection are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2, 1}, new[] {5, 3, 2, 1, 0}, @@ -150,7 +148,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2, 1}, new[] {4, 3, 2, 1, 0}, @@ -160,7 +158,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} // 0 get assembled and no further processing is done }, @@ -169,7 +167,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3, 4, 5}, // 2 and 4 get assembled inside InitializeCollectionFromCache new[] {3, 5, 0} @@ -179,7 +177,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2, 1}, // 4 and 2 get assembled inside InitializeCollectionFromCache new[] {3, 1, 0} @@ -189,7 +187,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4}, // 1 and 3 get assembled inside InitializeCollectionFromCache new[] {2, 4, 5} @@ -199,7 +197,7 @@ public async Task MultipleGetReadOnlyCollectionTestAsync() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2, 1}, // 5, 3 and 1 get assembled inside InitializeCollectionFromCache new[] {2, 0} @@ -221,7 +219,6 @@ public async Task MultipleGetReadOnlyTestAsync() var persister = Sfi.GetEntityPersister(typeof(ReadOnly).FullName); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -238,7 +235,7 @@ public async Task MultipleGetReadOnlyTestAsync() // DefaultLoadEventListener and the other time in BatchingEntityLoader. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2}, // triggered by LoadFromSecondLevelCache method of DefaultLoadEventListener type new[] {1, 2, 3}, // triggered by Load method of BatchingEntityLoader type @@ -250,7 +247,7 @@ public async Task MultipleGetReadOnlyTestAsync() // the nearest before the demanded entity are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3}, new[] {5, 3, 2}, @@ -260,7 +257,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3}, new[] {4, 3, 2}, @@ -270,7 +267,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2} // 0 get assembled and no further processing is done }, @@ -279,7 +276,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3}, // 2 gets assembled inside LoadFromSecondLevelCache new[] {3, 4, 5} @@ -289,7 +286,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3}, // 4 gets assembled inside LoadFromSecondLevelCache new[] {3, 2, 1} @@ -299,7 +296,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2}, // 1 gets assembled inside LoadFromSecondLevelCache new[] {2, 3, 4} @@ -309,7 +306,7 @@ public async Task MultipleGetReadOnlyTestAsync() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3}, // 5 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 1, 0} @@ -331,7 +328,6 @@ public async Task MultipleGetReadOnlyItemTestAsync() var persister = Sfi.GetEntityPersister(typeof(ReadOnlyItem).FullName); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -348,7 +344,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() // DefaultLoadEventListener and the other time in BatchingEntityLoader. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3}, // triggered by LoadFromSecondLevelCache method of DefaultLoadEventListener type new[] {1, 2, 3, 4}, // triggered by Load method of BatchingEntityLoader type @@ -360,7 +356,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() // the nearest before the demanded entity are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2}, new[] {5, 3, 2, 1}, @@ -370,7 +366,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2}, new[] {4, 3, 2, 1}, @@ -380,7 +376,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3} // 0 get assembled and no further processing is done }, @@ -389,7 +385,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3, 4}, // 2 and 4 get assembled inside LoadFromSecondLevelCache new[] {3, 5, 0} @@ -399,7 +395,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2}, // 4 and 2 get assembled inside LoadFromSecondLevelCache new[] {3, 1, 0} @@ -409,7 +405,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3}, // 1 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 4, 5} @@ -419,7 +415,7 @@ public async Task MultipleGetReadOnlyItemTestAsync() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2}, // 5 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 1, 0} @@ -459,7 +455,7 @@ public async Task MultiplePutReadWriteTestAsync() AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -468,7 +464,7 @@ public async Task MultiplePutReadWriteTestAsync() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -477,7 +473,7 @@ public async Task MultiplePutReadWriteTestAsync() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -514,7 +510,7 @@ public async Task MultiplePutReadWriteItemTestAsync() AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -522,7 +518,7 @@ public async Task MultiplePutReadWriteItemTestAsync() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -530,7 +526,7 @@ public async Task MultiplePutReadWriteItemTestAsync() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -542,33 +538,240 @@ public async Task MultiplePutReadWriteItemTestAsync() public async Task UpdateTimestampsCacheTestAsync() { var timestamp = Sfi.UpdateTimestampsCache; + var fieldReadonly = typeof(UpdateTimestampsCache).GetField( + "_batchReadOnlyUpdateTimestamps", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(fieldReadonly, Is.Not.Null, "Unable to find _batchReadOnlyUpdateTimestamps field"); + Assert.That(fieldReadonly.GetValue(timestamp), Is.Not.Null, "_batchReadOnlyUpdateTimestamps is null"); var field = typeof(UpdateTimestampsCache).GetField( "_batchUpdateTimestamps", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.That(field, Is.Not.Null); + Assert.That(field, Is.Not.Null, "Unable to find _batchUpdateTimestamps field"); var cache = (BatchableCache) field.GetValue(timestamp); - Assert.That(cache, Is.Not.Null); + Assert.That(cache, Is.Not.Null, "_batchUpdateTimestamps is null"); + await (cache.ClearAsync(CancellationToken.None)); + cache.ClearStatistics(); + + const string query = "from ReadOnly e where e.Name = :name"; + const string name = "Name1"; using (var s = OpenSession()) + using (var t = s.BeginTransaction()) { - const string query = "from ReadOnly e where e.Name = :name"; - const string name = "Name1"; await (s .CreateQuery(query) .SetString("name", name) .SetCacheable(true) .UniqueResultAsync()); + await (t.CommitAsync()); + } - // Run a second time, just to test the query cache - var result = await (s - .CreateQuery(query) - .SetString("name", name) - .SetCacheable(true) - .UniqueResultAsync()); + // Run a second time, to test the query cache + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = + await (s + .CreateQuery(query) + .SetString("name", name) + .SetCacheable(true) + .UniqueResultAsync()); Assert.That(result, Is.Not.Null); - Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(1)); - Assert.That(cache.GetCalls, Has.Count.EqualTo(0)); + await (t.CommitAsync()); + } + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(1), "GetMany"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "Get"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(0), "PutMany"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "Put"); + + // Update entities to put some update ts + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var readwrite1 = await (s.Query().FirstAsync()); + readwrite1.Name = "NewName"; + await (t.CommitAsync()); + } + // PreInvalidate + Invalidate => 2 calls + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(2), "PutMany after update"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "Put after update"); + } + + [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"); + + var queryCache = Sfi.GetQueryCache(null); + var readonlyField = typeof(StandardQueryCache).GetField( + "_batchableReadOnlyCache", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(readonlyField, Is.Not.Null, "Unable to find _batchableReadOnlyCache field"); + Assert.That(readonlyField.GetValue(queryCache), Is.Not.Null, "_batchableReadOnlyCache is null"); + var field = typeof(StandardQueryCache).GetField( + "_batchableCache", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(field, Is.Not.Null, "Unable to find _batchableCache field"); + var cache = (BatchableCache) field.GetValue(queryCache); + Assert.That(cache, Is.Not.Null, "_batchableCache is null"); + + var timestamp = Sfi.UpdateTimestampsCache; + var tsField = typeof(UpdateTimestampsCache).GetField( + "_batchUpdateTimestamps", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(tsField, Is.Not.Null, "Unable to find _batchUpdateTimestamps field"); + var tsCache = (BatchableCache) tsField.GetValue(timestamp); + Assert.That(tsCache, Is.Not.Null, "_batchUpdateTimestamps is null"); + + await (cache.ClearAsync(CancellationToken.None)); + cache.ClearStatistics(); + await (tsCache.ClearAsync(CancellationToken.None)); + tsCache.ClearStatistics(); + + 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(CancellationToken.None)); + await (t.CommitAsync()); + } + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(1), "cache GetMany first execution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get first execution"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(1), "cache PutMany first execution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put first execution"); + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(0), "tsCache GetMany first execution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get first execution"); + + // Run a second time, to test the query cache + using (var t = s.BeginTransaction()) + { + await (queries.ExecuteAsync(CancellationToken.None)); + await (t.CommitAsync()); + } + + Assert.That( + await (queries.GetResultAsync(0, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1"); + Assert.That( + await (queries.GetResultAsync(1, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2"); + Assert.That( + await (queries.GetResultAsync(2, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name3), "q3"); + Assert.That( + await (queries.GetResultAsync(3, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4"); + Assert.That( + await (queries.GetResultAsync(4, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5"); + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(2), "cache GetMany secondExecution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get secondExecution"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(1), "cache PutMany secondExecution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put secondExecution"); + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(1), "tsCache GetMany secondExecution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get secondExecution"); + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(0), "tsCache PutMany secondExecution"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put secondExecution"); + + // 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()); + } + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(1), "tsCache GetMany after update"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get after update"); + // Pre-invalidate + invalidate => 2 calls + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(2), "tsCache PutMany after update"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put after update"); + + // Run a third time, to re-test the query cache + using (var t = s.BeginTransaction()) + { + await (queries.ExecuteAsync(CancellationToken.None)); + await (t.CommitAsync()); + } + + Assert.That( + await (queries.GetResultAsync(0, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name1), "q1 after update"); + Assert.That( + await (queries.GetResultAsync(1, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name2), "q2 after update"); + Assert.That( + await (queries.GetResultAsync(2, CancellationToken.None)), + Has.Count.EqualTo(0), "q3 after update"); + Assert.That( + await (queries.GetResultAsync(3, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadWrite.Name)).EqualTo(name4), "q4 after update"); + Assert.That( + await (queries.GetResultAsync(4, CancellationToken.None)), + Has.Count.EqualTo(1).And.One.Property(nameof(ReadOnly.Name)).EqualTo(name5), "q5 after update"); + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(3), "cache GetMany thirdExecution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get thirdExecution"); + // ReadWrite queries should have been re-put, so count should have been incremented + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(2), "cache PutMany thirdExecution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put thirdExecution"); + + // Readonly entities should have been still cached, so their queries timestamp should have been + // rechecked and the get count incremented + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(2), "tsCache GetMany thirdExecution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get thirdExecution"); + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(2), "tsCache PutMany thirdExecution"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put thirdExecution"); } } diff --git a/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs b/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs index 2cc850b8e97..fbe948fb8c1 100644 --- a/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs +++ b/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs @@ -304,7 +304,7 @@ public async Task SerializationAsync() var transformer = Transformers.AliasToBean(); var bytes = SerializationHelper.Serialize(transformer); transformer = (IResultTransformer)SerializationHelper.Deserialize(bytes); - return AssertCardinalityNameAndIdAsync(transformer: transformer, cancellationToken:cancellationToken); + return AssertCardinalityNameAndIdAsync(transformer: transformer, cancellationToken: cancellationToken); } catch (System.Exception ex) { diff --git a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs index 97afbebcb74..9e166ef6509 100644 --- a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs @@ -1,13 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using NHibernate.Cache; using NHibernate.Cfg; -using NHibernate.DomainModel; +using NHibernate.Linq; +using NHibernate.Multi; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; using Environment = NHibernate.Cfg.Environment; @@ -34,12 +32,6 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); } - protected override bool CheckDatabaseWasCleaned() - { - base.CheckDatabaseWasCleaned(); - return true; // We are unable to delete read-only items. - } - protected override void OnSetUp() { using (var s = Sfi.OpenSession()) @@ -87,9 +79,16 @@ protected override void OnTearDown() using (var s = OpenSession()) using (var tx = s.BeginTransaction()) { - s.Delete("from ReadWrite"); + 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 manually evict "readonly" entities since their caches are readonly + Sfi.Evict(typeof(ReadOnly)); + Sfi.Evict(typeof(ReadOnlyItem)); + Sfi.EvictQueries(); } [Test] @@ -98,7 +97,6 @@ public void MultipleGetReadOnlyCollectionTest() var persister = Sfi.GetCollectionPersister($"{typeof(ReadOnly).FullName}.Items"); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -116,7 +114,7 @@ public void MultipleGetReadOnlyCollectionTest() // DefaultInitializeCollectionEventListener and the other time in BatchingCollectionInitializer. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4}, // triggered by InitializeCollectionFromCache method of DefaultInitializeCollectionEventListener type new[] {1, 2, 3, 4, 5}, // triggered by Initialize method of BatchingCollectionInitializer type @@ -128,7 +126,7 @@ public void MultipleGetReadOnlyCollectionTest() // the nearest before the demanded collection are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2, 1}, new[] {5, 3, 2, 1, 0}, @@ -138,7 +136,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2, 1}, new[] {4, 3, 2, 1, 0}, @@ -148,7 +146,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} // 0 get assembled and no further processing is done }, @@ -157,7 +155,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3, 4, 5}, // 2 and 4 get assembled inside InitializeCollectionFromCache new[] {3, 5, 0} @@ -167,7 +165,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2, 1}, // 4 and 2 get assembled inside InitializeCollectionFromCache new[] {3, 1, 0} @@ -177,7 +175,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3, 4}, // 1 and 3 get assembled inside InitializeCollectionFromCache new[] {2, 4, 5} @@ -187,7 +185,7 @@ public void MultipleGetReadOnlyCollectionTest() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2, 1}, // 5, 3 and 1 get assembled inside InitializeCollectionFromCache new[] {2, 0} @@ -209,7 +207,6 @@ public void MultipleGetReadOnlyTest() var persister = Sfi.GetEntityPersister(typeof(ReadOnly).FullName); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -226,7 +223,7 @@ public void MultipleGetReadOnlyTest() // DefaultLoadEventListener and the other time in BatchingEntityLoader. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2}, // triggered by LoadFromSecondLevelCache method of DefaultLoadEventListener type new[] {1, 2, 3}, // triggered by Load method of BatchingEntityLoader type @@ -238,7 +235,7 @@ public void MultipleGetReadOnlyTest() // the nearest before the demanded entity are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3}, new[] {5, 3, 2}, @@ -248,7 +245,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3}, new[] {4, 3, 2}, @@ -258,7 +255,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2} // 0 get assembled and no further processing is done }, @@ -267,7 +264,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3}, // 2 gets assembled inside LoadFromSecondLevelCache new[] {3, 4, 5} @@ -277,7 +274,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3}, // 4 gets assembled inside LoadFromSecondLevelCache new[] {3, 2, 1} @@ -287,7 +284,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2}, // 1 gets assembled inside LoadFromSecondLevelCache new[] {2, 3, 4} @@ -297,7 +294,7 @@ public void MultipleGetReadOnlyTest() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3}, // 5 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 1, 0} @@ -319,7 +316,6 @@ public void MultipleGetReadOnlyItemTest() var persister = Sfi.GetEntityPersister(typeof(ReadOnlyItem).FullName); Assert.That(persister.Cache.Cache, Is.Not.Null); Assert.That(persister.Cache.Cache, Is.TypeOf()); - var cache = (BatchableCache) persister.Cache.Cache; var ids = new List(); using (var s = Sfi.OpenSession()) @@ -336,7 +332,7 @@ public void MultipleGetReadOnlyItemTest() // DefaultLoadEventListener and the other time in BatchingEntityLoader. new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3}, // triggered by LoadFromSecondLevelCache method of DefaultLoadEventListener type new[] {1, 2, 3, 4}, // triggered by Load method of BatchingEntityLoader type @@ -348,7 +344,7 @@ public void MultipleGetReadOnlyItemTest() // the nearest before the demanded entity are added. new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2}, new[] {5, 3, 2, 1}, @@ -358,7 +354,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2}, new[] {4, 3, 2, 1}, @@ -368,7 +364,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3} // 0 get assembled and no further processing is done }, @@ -377,7 +373,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 1, - new int[][] + new[] { new[] {1, 2, 3, 4}, // 2 and 4 get assembled inside LoadFromSecondLevelCache new[] {3, 5, 0} @@ -387,7 +383,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 5, - new int[][] + new[] { new[] {5, 4, 3, 2}, // 4 and 2 get assembled inside LoadFromSecondLevelCache new[] {3, 1, 0} @@ -397,7 +393,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 0, - new int[][] + new[] { new[] {0, 1, 2, 3}, // 1 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 4, 5} @@ -407,7 +403,7 @@ public void MultipleGetReadOnlyItemTest() ), new Tuple>( 4, - new int[][] + new[] { new[] {4, 5, 3, 2}, // 5 and 3 get assembled inside LoadFromSecondLevelCache new[] {2, 1, 0} @@ -447,7 +443,7 @@ public void MultiplePutReadWriteTest() AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -456,7 +452,7 @@ public void MultiplePutReadWriteTest() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -465,7 +461,7 @@ public void MultiplePutReadWriteTest() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2}, new[] {3, 4, 5} @@ -502,7 +498,7 @@ public void MultiplePutReadWriteItemTest() AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -510,7 +506,7 @@ public void MultiplePutReadWriteItemTest() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -518,7 +514,7 @@ public void MultiplePutReadWriteItemTest() ); AssertEquivalent( ids, - new int[][] + new[] { new[] {0, 1, 2, 3, 4} }, @@ -530,33 +526,240 @@ public void MultiplePutReadWriteItemTest() public void UpdateTimestampsCacheTest() { var timestamp = Sfi.UpdateTimestampsCache; + var fieldReadonly = typeof(UpdateTimestampsCache).GetField( + "_batchReadOnlyUpdateTimestamps", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(fieldReadonly, Is.Not.Null, "Unable to find _batchReadOnlyUpdateTimestamps field"); + Assert.That(fieldReadonly.GetValue(timestamp), Is.Not.Null, "_batchReadOnlyUpdateTimestamps is null"); var field = typeof(UpdateTimestampsCache).GetField( "_batchUpdateTimestamps", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.That(field, Is.Not.Null); + Assert.That(field, Is.Not.Null, "Unable to find _batchUpdateTimestamps field"); var cache = (BatchableCache) field.GetValue(timestamp); - Assert.That(cache, Is.Not.Null); + Assert.That(cache, Is.Not.Null, "_batchUpdateTimestamps is null"); + cache.Clear(); + cache.ClearStatistics(); + + const string query = "from ReadOnly e where e.Name = :name"; + const string name = "Name1"; using (var s = OpenSession()) + using (var t = s.BeginTransaction()) { - const string query = "from ReadOnly e where e.Name = :name"; - const string name = "Name1"; s .CreateQuery(query) .SetString("name", name) .SetCacheable(true) .UniqueResult(); + t.Commit(); + } - // Run a second time, just to test the query cache - var result = s - .CreateQuery(query) - .SetString("name", name) - .SetCacheable(true) - .UniqueResult(); + // Run a second time, to test the query cache + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = + s + .CreateQuery(query) + .SetString("name", name) + .SetCacheable(true) + .UniqueResult(); Assert.That(result, Is.Not.Null); - Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(1)); - Assert.That(cache.GetCalls, Has.Count.EqualTo(0)); + t.Commit(); + } + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(1), "GetMany"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "Get"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(0), "PutMany"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "Put"); + + // Update entities to put some update ts + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var readwrite1 = s.Query().First(); + readwrite1.Name = "NewName"; + t.Commit(); + } + // PreInvalidate + Invalidate => 2 calls + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(2), "PutMany after update"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "Put after update"); + } + + [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"); + + var queryCache = Sfi.GetQueryCache(null); + var readonlyField = typeof(StandardQueryCache).GetField( + "_batchableReadOnlyCache", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(readonlyField, Is.Not.Null, "Unable to find _batchableReadOnlyCache field"); + Assert.That(readonlyField.GetValue(queryCache), Is.Not.Null, "_batchableReadOnlyCache is null"); + var field = typeof(StandardQueryCache).GetField( + "_batchableCache", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(field, Is.Not.Null, "Unable to find _batchableCache field"); + var cache = (BatchableCache) field.GetValue(queryCache); + Assert.That(cache, Is.Not.Null, "_batchableCache is null"); + + var timestamp = Sfi.UpdateTimestampsCache; + var tsField = typeof(UpdateTimestampsCache).GetField( + "_batchUpdateTimestamps", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(tsField, Is.Not.Null, "Unable to find _batchUpdateTimestamps field"); + var tsCache = (BatchableCache) tsField.GetValue(timestamp); + Assert.That(tsCache, Is.Not.Null, "_batchUpdateTimestamps is null"); + + cache.Clear(); + cache.ClearStatistics(); + tsCache.Clear(); + tsCache.ClearStatistics(); + + 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(cache.GetMultipleCalls, Has.Count.EqualTo(1), "cache GetMany first execution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get first execution"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(1), "cache PutMany first execution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put first execution"); + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(0), "tsCache GetMany first execution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get 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(cache.GetMultipleCalls, Has.Count.EqualTo(2), "cache GetMany secondExecution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get secondExecution"); + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(1), "cache PutMany secondExecution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put secondExecution"); + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(1), "tsCache GetMany secondExecution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get secondExecution"); + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(0), "tsCache PutMany secondExecution"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put secondExecution"); + + // 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(); + } + + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(1), "tsCache GetMany after update"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get after update"); + // Pre-invalidate + invalidate => 2 calls + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(2), "tsCache PutMany after update"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put after update"); + + // 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"); + + Assert.That(cache.GetMultipleCalls, Has.Count.EqualTo(3), "cache GetMany thirdExecution"); + Assert.That(cache.GetCalls, Has.Count.EqualTo(0), "cache Get thirdExecution"); + // ReadWrite queries should have been re-put, so count should have been incremented + Assert.That(cache.PutMultipleCalls, Has.Count.EqualTo(2), "cache PutMany thirdExecution"); + Assert.That(cache.PutCalls, Has.Count.EqualTo(0), "cache Put thirdExecution"); + + // Readonly entities should have been still cached, so their queries timestamp should have been + // rechecked and the get count incremented + Assert.That(tsCache.GetMultipleCalls, Has.Count.EqualTo(2), "tsCache GetMany thirdExecution"); + Assert.That(tsCache.GetCalls, Has.Count.EqualTo(0), "tsCache Get thirdExecution"); + Assert.That(tsCache.PutMultipleCalls, Has.Count.EqualTo(2), "tsCache PutMany thirdExecution"); + Assert.That(tsCache.PutCalls, Has.Count.EqualTo(0), "tsCache Put thirdExecution"); } } diff --git a/src/NHibernate/Async/Cache/CacheBatcher.cs b/src/NHibernate/Async/Cache/CacheBatcher.cs index bce22267614..de7fa2228b9 100644 --- a/src/NHibernate/Async/Cache/CacheBatcher.cs +++ b/src/NHibernate/Async/Cache/CacheBatcher.cs @@ -8,11 +8,7 @@ //------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Text; -using NHibernate.Cache.Access; using NHibernate.Engine; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; @@ -21,7 +17,7 @@ namespace NHibernate.Cache { using System.Threading.Tasks; using System.Threading; - internal partial class CacheBatcher + public sealed partial class CacheBatcher { /// @@ -31,7 +27,7 @@ internal partial class CacheBatcher /// The entity persister. /// The data to put in the cache. /// A cancellation token that can be used to cancel the work - public async Task AddToBatchAsync(IEntityPersister persister, CachePutData data, CancellationToken cancellationToken) + internal async Task AddToBatchAsync(IEntityPersister persister, CachePutData data, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (ShouldExecuteBatch(persister, _putBatch)) @@ -54,7 +50,7 @@ public async Task AddToBatchAsync(IEntityPersister persister, CachePutData data, /// The collection persister. /// The data to put in the cache. /// A cancellation token that can be used to cancel the work - public async Task AddToBatchAsync(ICollectionPersister persister, CachePutData data, CancellationToken cancellationToken) + internal async Task AddToBatchAsync(ICollectionPersister persister, CachePutData data, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (ShouldExecuteBatch(persister, _putBatch)) @@ -74,7 +70,7 @@ public async Task AddToBatchAsync(ICollectionPersister persister, CachePutData d /// Executes the current batch. /// /// A cancellation token that can be used to cancel the work - public async Task ExecuteBatchAsync(CancellationToken cancellationToken) + internal async Task ExecuteBatchAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (_currentBatch == null || _currentBatch.BatchSize == 0) diff --git a/src/NHibernate/Async/Cache/IQueryCache.cs b/src/NHibernate/Async/Cache/IQueryCache.cs index 8ac8d1bfb24..5cb2aded412 100644 --- a/src/NHibernate/Async/Cache/IQueryCache.cs +++ b/src/NHibernate/Async/Cache/IQueryCache.cs @@ -8,6 +8,7 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; using System.Collections.Generic; using NHibernate.Engine; @@ -20,8 +21,275 @@ namespace NHibernate.Cache public partial interface IQueryCache { + /// + /// Clear the cache. + /// + /// A cancellation token that can be used to cancel the work Task ClearAsync(CancellationToken cancellationToken); + + // Since 5.2 + [Obsolete("Have the query cache implement IBatchableQueryCache, and use IBatchableQueryCache.Put")] Task PutAsync(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session, CancellationToken cancellationToken); + + // Since 5.2 + [Obsolete("Have the query cache implement IBatchableQueryCache, and use IBatchableQueryCache.Get")] Task GetAsync(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet spaces, ISessionImplementor session, CancellationToken cancellationToken); } + + public partial interface IBatchableQueryCache : IQueryCache + { + /// + /// Get query results from the cache. + /// + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query spaces. + /// The session for which the query is executed. + /// A cancellation token that can be used to cancel the work + /// The query results, if cached. + Task GetAsync( + QueryKey key, QueryParameters queryParameters, ICacheAssembler[] returnTypes, ISet spaces, + ISessionImplementor session, CancellationToken cancellationToken); + + /// + /// Put query results in the cache. + /// + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query result. + /// The session for which the query was executed. + /// A cancellation token that can be used to cancel the work + /// if the result has been cached, + /// otherwise. + Task PutAsync( + QueryKey key, QueryParameters queryParameters, ICacheAssembler[] returnTypes, IList result, + ISessionImplementor session, CancellationToken cancellationToken); + + /// + /// Retrieve multiple query results from the cache. + /// + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query spaces matching . + /// The session for which the queries are executed. + /// A cancellation token that can be used to cancel the work + /// The cached query results, matching each key of respectively. For each + /// missed key, it will contain a . + Task GetManyAsync( + QueryKey[] keys, QueryParameters[] queryParameters, ICacheAssembler[][] returnTypes, + ISet[] spaces, ISessionImplementor session, CancellationToken cancellationToken); + + /// + /// Attempt to cache objects, after loading them from the database. + /// + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query results matching . + /// The session for which the queries were executed. + /// A cancellation token that can be used to cancel the work + /// An array of boolean indicating if each query was successfully cached. + /// + Task PutManyAsync( + QueryKey[] keys, QueryParameters[] queryParameters, ICacheAssembler[][] returnTypes, IList[] results, + ISessionImplementor session, CancellationToken cancellationToken); + } + + internal static partial class QueryCacheExtensions + { + + /// + /// Get query results from the cache. + /// + /// The cache. + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query spaces. + /// The session for which the query is executed. + /// A cancellation token that can be used to cancel the work + /// The query results, if cached. + public static async Task GetAsync( + this IQueryCache queryCache, + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + ISet spaces, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return await (batchableQueryCache.GetAsync( + key, + queryParameters, + returnTypes, + spaces, + session, cancellationToken)).ConfigureAwait(false); + } + + if (!_hasWarnForObsoleteQueryCache) + { + _hasWarnForObsoleteQueryCache = true; + Log.Warn("{0} is obsolete, it should implement {1}", queryCache, nameof(IBatchableQueryCache)); + } + + var persistenceContext = session.PersistenceContext; + + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + + if (queryParameters.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; + else + queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + + try + { +#pragma warning disable 618 + return await (queryCache.GetAsync( +#pragma warning restore 618 + key, + returnTypes, + queryParameters.NaturalKeyLookup, + spaces, + session, cancellationToken)).ConfigureAwait(false); + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + /// + /// Put query results in the cache. + /// + /// The cache. + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query result. + /// The session for which the query was executed. + /// A cancellation token that can be used to cancel the work + /// if the result has been cached, + /// otherwise. + public static Task PutAsync( + this IQueryCache queryCache, + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + IList result, + ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return batchableQueryCache.PutAsync( + key, queryParameters, + returnTypes, + result, session, cancellationToken); + } + +#pragma warning disable 618 + return queryCache.PutAsync( +#pragma warning restore 618 + key, + returnTypes, + result, + queryParameters.NaturalKeyLookup, + session, cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Retrieve multiple query results from the cache. + /// + /// The cache. + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query spaces matching . + /// The session for which the queries are executed. + /// A cancellation token that can be used to cancel the work + /// The cached query results, matching each key of respectively. For each + /// missed key, it will contain a . + public static async Task GetManyAsync( + this IQueryCache queryCache, + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + ISet[] spaces, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return await (batchableQueryCache.GetManyAsync( + keys, + queryParameters, + returnTypes, + spaces, + session, cancellationToken)).ConfigureAwait(false); + } + + var results = new IList[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + results[i] = await (queryCache.GetAsync(keys[i], queryParameters[i], returnTypes[i], spaces[i], session, cancellationToken)).ConfigureAwait(false); + } + + return results; + } + + /// + /// Attempt to cache objects, after loading them from the database. + /// + /// The cache. + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query results matching . + /// The session for which the queries were executed. + /// A cancellation token that can be used to cancel the work + /// An array of boolean indicating if each query was successfully cached. + /// + public static async Task PutManyAsync( + this IQueryCache queryCache, + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + IList[] results, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return await (batchableQueryCache.PutManyAsync( + keys, + queryParameters, + returnTypes, + results, + session, cancellationToken)).ConfigureAwait(false); + } + + var puts = new bool[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + puts[i] = await (queryCache.PutAsync(keys[i], queryParameters[i], returnTypes[i], results[i], session, cancellationToken)).ConfigureAwait(false); + } + + return puts; + } + } } diff --git a/src/NHibernate/Async/Cache/StandardQueryCache.cs b/src/NHibernate/Async/Cache/StandardQueryCache.cs index a85a0916311..81a3dfabe92 100644 --- a/src/NHibernate/Async/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Async/Cache/StandardQueryCache.cs @@ -11,7 +11,7 @@ using System; using System.Collections; using System.Collections.Generic; - +using System.Linq; using NHibernate.Cfg; using NHibernate.Engine; using NHibernate.Type; @@ -21,7 +21,7 @@ namespace NHibernate.Cache { using System.Threading.Tasks; using System.Threading; - public partial class StandardQueryCache : IQueryCache + public partial class StandardQueryCache : IQueryCache, IBatchableQueryCache { #region IQueryCache Members @@ -35,35 +35,73 @@ public Task ClearAsync(CancellationToken cancellationToken) return _queryCache.ClearAsync(cancellationToken); } + /// + public Task PutAsync( + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + IList result, + ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + // 6.0 TODO: inline the call. +#pragma warning disable 612 + return PutAsync(key, returnTypes, result, queryParameters.NaturalKeyLookup, session, cancellationToken); +#pragma warning restore 612 + } + + // Since 5.2 + [Obsolete] public async Task PutAsync(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (isNaturalKeyLookup && result.Count == 0) return false; - long ts = session.Factory.Settings.CacheProvider.NextTimestamp(); + var ts = session.Factory.Settings.CacheProvider.NextTimestamp(); - if (Log.IsDebugEnabled()) - Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); - - IList cacheable = new List(result.Count + 1) {ts}; - for (int i = 0; i < result.Count; i++) - { - if (returnTypes.Length == 1) - { - cacheable.Add(await (returnTypes[0].DisassembleAsync(result[i], session, null, cancellationToken)).ConfigureAwait(false)); - } - else - { - cacheable.Add(await (TypeHelper.DisassembleAsync((object[]) result[i], returnTypes, null, session, null, cancellationToken)).ConfigureAwait(false)); - } - } + Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); - await (_queryCache.PutAsync(key, cacheable, cancellationToken)).ConfigureAwait(false); + await (_queryCache.PutAsync(key, await (GetCacheableResultAsync(returnTypes, session, result, ts, cancellationToken)).ConfigureAwait(false), cancellationToken)).ConfigureAwait(false); return true; } + /// + public async Task GetAsync( + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + ISet spaces, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var persistenceContext = session.PersistenceContext; + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + + if (queryParameters.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; + else + queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + + 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 + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + // Since 5.2 + [Obsolete] public async Task GetAsync(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet spaces, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -88,63 +126,242 @@ public async Task GetAsync(QueryKey key, ICacheAssembler[] returnTypes, b return null; } - Log.Debug("returning cached query results for: {0}", key); - if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) + return await (GetResultFromCacheableAsync(key, returnTypes, isNaturalKeyLookup, session, cacheable, cancellationToken)).ConfigureAwait(false); + } + + /// + public async Task PutManyAsync( + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + IList[] results, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var cached = new bool[keys.Length]; + if (_batchableCache == null) { - returnTypes = GuessTypes(cacheable); + for (var i = 0; i < keys.Length; i++) + { + cached[i] = await (PutAsync(keys[i], queryParameters[i], returnTypes[i], results[i], session, cancellationToken)).ConfigureAwait(false); + } + return cached; } - for (int i = 1; i < cacheable.Count; i++) + var ts = session.Factory.Settings.CacheProvider.NextTimestamp(); + + if (Log.IsDebugEnabled()) + Log.Debug("caching query results in region: '{0}'; {1}", _regionName, StringHelper.CollectionToString(keys)); + + var cachedKeys = new List(); + var cachedResults = new List(); + for (var i = 0; i < keys.Length; i++) + { + var result = results[i]; + if (queryParameters[i].NaturalKeyLookup && result.Count == 0) + continue; + + cached[i] = true; + cachedKeys.Add(keys[i]); + cachedResults.Add(await (GetCacheableResultAsync(returnTypes[i], session, result, ts, cancellationToken)).ConfigureAwait(false)); + } + + await (_batchableCache.PutManyAsync(cachedKeys.ToArray(), cachedResults.ToArray(), cancellationToken)).ConfigureAwait(false); + + return cached; + } + + /// + public async Task GetManyAsync( + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + ISet[] spaces, + ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var results = new IList[keys.Length]; + if (_batchableReadOnlyCache == null) + { + for (var i = 0; i < keys.Length; i++) + { + results[i] = await (GetAsync(keys[i], queryParameters[i], returnTypes[i], spaces[i], session, cancellationToken)).ConfigureAwait(false); + } + return results; + } + + if (Log.IsDebugEnabled()) + Log.Debug("checking cached query results in region: '{0}'; {1}", _regionName, StringHelper.CollectionToString(keys)); + + var cacheables = (await (_batchableReadOnlyCache.GetManyAsync(keys, cancellationToken)).ConfigureAwait(false)).Cast().ToArray(); + + var spacesToCheck = new List>(); + var checkedSpacesIndexes = new HashSet(); + var checkedSpacesTimestamp = new List(); + for (var i = 0; i < keys.Length; i++) + { + var cacheable = cacheables[i]; + if (cacheable == null) + { + Log.Debug("query results were not found in cache: {0}", keys[i]); + continue; + } + + 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]); + if (Log.IsDebugEnabled()) + Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(querySpaces)); + } + + var upToDates = spacesToCheck.Count > 0 + ? await (_updateTimestampsCache.AreUpToDateAsync(spacesToCheck.ToArray(), checkedSpacesTimestamp.ToArray(), cancellationToken)).ConfigureAwait(false) + : Array.Empty(); + + var upToDatesIndex = 0; + var persistenceContext = session.PersistenceContext; + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + for (var i = 0; i < keys.Length; i++) + { + var cacheable = cacheables[i]; + if (cacheable == null) + continue; + + var key = keys[i]; + if (checkedSpacesIndexes.Contains(i) && !upToDates[upToDatesIndex++]) + { + Log.Debug("cached query results were not up to date for: {0}", key); + continue; + } + + var queryParams = queryParameters[i]; + if (queryParams.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParams.ReadOnly; + else + queryParams.ReadOnly = persistenceContext.DefaultReadOnly; + + try + { + results[i] = await (GetResultFromCacheableAsync(key, returnTypes[i], queryParams.NaturalKeyLookup, session, cacheable, cancellationToken)).ConfigureAwait(false); + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + return results; + } + + #endregion + + private static async Task> GetCacheableResultAsync( + ICacheAssembler[] returnTypes, + ISessionImplementor session, + IList result, + long ts, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var cacheable = new List(result.Count + 1) { ts }; + foreach (var row in result) { if (returnTypes.Length == 1) { - var beforeAssembleTask = returnTypes[0]?.BeforeAssembleAsync(cacheable[i], session, cancellationToken); - if (beforeAssembleTask != null) - { - await (beforeAssembleTask).ConfigureAwait(false); - } + cacheable.Add(await (returnTypes[0].DisassembleAsync(row, session, null, cancellationToken)).ConfigureAwait(false)); } else { - await (TypeHelper.BeforeAssembleAsync((object[])cacheable[i], returnTypes, session, cancellationToken)).ConfigureAwait(false); + cacheable.Add(await (TypeHelper.DisassembleAsync((object[])row, returnTypes, null, session, null, cancellationToken)).ConfigureAwait(false)); } } - IList result = new List(cacheable.Count - 1); - for (int i = 1; i < cacheable.Count; i++) + return cacheable; + } + + private async Task GetResultFromCacheableAsync( + QueryKey key, + ICacheAssembler[] returnTypes, + bool isNaturalKeyLookup, + ISessionImplementor session, + IList cacheable, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Log.Debug("returning cached query results for: {0}", key); + if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) + { + returnTypes = GuessTypes(cacheable); + } + + try { - try + var result = new List(cacheable.Count - 1); + if (returnTypes.Length == 1) { - if (returnTypes.Length == 1) + 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++) { - result.Add(await (returnTypes[0].AssembleAsync(cacheable[i], session, null, cancellationToken)).ConfigureAwait(false)); + rows.Add(cacheable[i]); } - else + + foreach (var row in rows) { - result.Add(await (TypeHelper.AssembleAsync((object[])cacheable[i], returnTypes, session, null, cancellationToken)).ConfigureAwait(false)); + await (returnType.BeforeAssembleAsync(row, session, cancellationToken)).ConfigureAwait(false); + } + + foreach (var row in rows) + { + result.Add(await (returnType.AssembleAsync(row, session, null, cancellationToken)).ConfigureAwait(false)); } } - catch (UnresolvableObjectException ex) + else { - if (isNaturalKeyLookup) + // Skip first element, it is the timestamp + var rows = new List(cacheable.Count - 1); + for (var i = 1; i < cacheable.Count; i++) { - //TODO: not really completely correct, since - // the UnresolvableObjectException could occur while resolving - // associations, leaving the PC in an inconsistent state - Log.Debug(ex, "could not reassemble cached result set"); - await (_queryCache.RemoveAsync(key, cancellationToken)).ConfigureAwait(false); - return null; + rows.Add((object[]) cacheable[i]); } - throw; + foreach (var row in rows) + { + await (TypeHelper.BeforeAssembleAsync(row, returnTypes, session, cancellationToken)).ConfigureAwait(false); + } + + foreach (var row in rows) + { + result.Add(await (TypeHelper.AssembleAsync(row, returnTypes, session, null, cancellationToken)).ConfigureAwait(false)); + } } + + return result; } + catch (UnresolvableObjectException ex) + { + if (isNaturalKeyLookup) + { + //TODO: not really completely correct, since + // the UnresolvableObjectException could occur while resolving + // associations, leaving the PC in an inconsistent state + Log.Debug(ex, "could not reassemble cached result set"); + // Handling a RemoveMany here does not look worth it, as this case short-circuits + // the result-set. So a Many could only benefit batched queries, and only if many + // of them are natural key lookup with an unresolvable object case. + await (_queryCache.RemoveAsync(key, cancellationToken)).ConfigureAwait(false); + return null; + } - return result; + throw; + } } - #endregion - protected virtual Task IsUpToDateAsync(ISet spaces, long timestamp, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs index 843160a1824..8c269608e76 100644 --- a/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs +++ b/src/NHibernate/Async/Cache/UpdateTimestampsCache.cs @@ -14,6 +14,7 @@ using System.Runtime.CompilerServices; using NHibernate.Cfg; +using NHibernate.Util; namespace NHibernate.Cache { @@ -24,6 +25,7 @@ public partial class UpdateTimestampsCache private readonly NHibernate.Util.AsyncLock _preInvalidate = new NHibernate.Util.AsyncLock(); private readonly NHibernate.Util.AsyncLock _invalidate = new NHibernate.Util.AsyncLock(); private readonly NHibernate.Util.AsyncLock _isUpToDate = new NHibernate.Util.AsyncLock(); + private readonly NHibernate.Util.AsyncLock _areUpToDate = new NHibernate.Util.AsyncLock(); public virtual Task ClearAsync(CancellationToken cancellationToken) { @@ -60,11 +62,8 @@ public virtual async Task PreInvalidateAsync(IReadOnlyCollection spaces, using (await _preInvalidate.LockAsync()) { //TODO: to handle concurrent writes correctly, this should return a Lock to the client - long ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; - foreach (var space in spaces) - { - await (updateTimestamps.PutAsync(space, ts, cancellationToken)).ConfigureAwait(false); - } + var ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; + await (SetSpacesTimestampAsync(spaces, ts, cancellationToken)).ConfigureAwait(false); //TODO: return new Lock(ts); } @@ -100,9 +99,32 @@ public virtual async Task InvalidateAsync(IReadOnlyCollection spaces, Ca //TODO: to handle concurrent writes correctly, the client should pass in a Lock long ts = updateTimestamps.NextTimestamp(); //TODO: if lock.getTimestamp().equals(ts) + if (log.IsDebugEnabled()) + log.Debug("Invalidating spaces [{0}]", StringHelper.CollectionToString(spaces)); + await (SetSpacesTimestampAsync(spaces, ts, cancellationToken)).ConfigureAwait(false); + } + } + + private async Task SetSpacesTimestampAsync(IReadOnlyCollection spaces, long ts, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_batchUpdateTimestamps != null) + { + if (spaces.Count == 0) + return; + + var timestamps = new object[spaces.Count]; + for (var i = 0; i < timestamps.Length; i++) + { + timestamps[i] = ts; + } + + await (_batchUpdateTimestamps.PutManyAsync(spaces.ToArray(), timestamps, cancellationToken)).ConfigureAwait(false); + } + else + { foreach (var space in spaces) { - log.Debug("Invalidating space [{0}]", space); await (updateTimestamps.PutAsync(space, ts, cancellationToken)).ConfigureAwait(false); } } @@ -114,35 +136,79 @@ public virtual async Task IsUpToDateAsync(ISet spaces, long timest cancellationToken.ThrowIfCancellationRequested(); using (await _isUpToDate.LockAsync()) { - if (_batchUpdateTimestamps != null) + if (_batchReadOnlyUpdateTimestamps != null) { + if (spaces.Count == 0) + return true; + var keys = new object[spaces.Count]; var index = 0; foreach (var space in spaces) { keys[index++] = space; } - var lastUpdates = await (_batchUpdateTimestamps.GetManyAsync(keys, cancellationToken)).ConfigureAwait(false); - foreach (var lastUpdate in lastUpdates) + var lastUpdates = await (_batchReadOnlyUpdateTimestamps.GetManyAsync(keys, cancellationToken)).ConfigureAwait(false); + return lastUpdates.All(lastUpdate => !IsOutdated(lastUpdate as long?, timestamp)); + } + + return spaces.Select(space => updateTimestamps.Get(space)) + .All(lastUpdate => !IsOutdated(lastUpdate as long?, timestamp)); + } + } + + [MethodImpl()] + public virtual async Task AreUpToDateAsync(ISet[] spaces, long[] timestamps, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + using (await _areUpToDate.LockAsync()) + { + var results = new bool[spaces.Length]; + var allSpaces = new HashSet(); + foreach (var sp in spaces) + { + allSpaces.UnionWith(sp); + } + + if (_batchReadOnlyUpdateTimestamps != null) + { + if (allSpaces.Count == 0) { - if (IsOutdated(lastUpdate, timestamp)) + for (var i = 0; i < spaces.Length; i++) { - return false; + results[i] = true; } + + return results; } - return true; - } - foreach (string space in spaces) + var keys = new object[allSpaces.Count]; + var index = 0; + foreach (var space in allSpaces) + { + keys[index++] = space; + } + + index = 0; + var lastUpdatesBySpace = + (await (_batchReadOnlyUpdateTimestamps + .GetManyAsync(keys, cancellationToken)).ConfigureAwait(false)) + .ToDictionary(u => keys[index++], u => u as long?); + + for (var i = 0; i < spaces.Length; i++) + { + var timestamp = timestamps[i]; + results[i] = spaces[i].All(space => !IsOutdated(lastUpdatesBySpace[space], timestamp)); + } + } + else { - object lastUpdate = await (updateTimestamps.GetAsync(space, cancellationToken)).ConfigureAwait(false); - if (IsOutdated(lastUpdate, timestamp)) + for (var i = 0; i < spaces.Length; i++) { - return false; + results[i] = await (IsUpToDateAsync(spaces[i], timestamps[i], cancellationToken)).ConfigureAwait(false); } - } - return true; + + return results; } } } diff --git a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs index f40733eedc5..66e293c3b08 100644 --- a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs @@ -117,7 +117,7 @@ private async Task ListUsingQueryCacheAsync(HashSet querySpaces, result = list; if (session.CacheMode.HasFlag(CacheMode.Put)) { - bool put = await (queryCache.PutAsync(key, new ICacheAssembler[] { assembler }, new object[] { list }, combinedParameters.NaturalKeyLookup, session, cancellationToken)).ConfigureAwait(false); + bool put = await (queryCache.PutAsync(key, combinedParameters, new ICacheAssembler[] { assembler }, new object[] { list }, session, cancellationToken)).ConfigureAwait(false); if (put && factory.Statistics.IsStatisticsEnabled) { factory.StatisticsImplementor.QueryCachePut(key.ToString(), queryCache.RegionName); @@ -200,7 +200,7 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken for (int i = 0; i < loaders.Count; i++) { CriteriaLoader loader = loaders[i]; - await (loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, session.DefaultReadOnly, cancellationToken)).ConfigureAwait(false); + await (loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, session.DefaultReadOnly, cancellationToken: cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { diff --git a/src/NHibernate/Async/Impl/MultiQueryImpl.cs b/src/NHibernate/Async/Impl/MultiQueryImpl.cs index 35a964ca0c3..93cf48d59ad 100644 --- a/src/NHibernate/Async/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Async/Impl/MultiQueryImpl.cs @@ -172,7 +172,7 @@ protected async Task> DoListAsync(CancellationToken cancellationTok ITranslator translator = translators[i]; QueryParameters parameter = parameters[i]; - await (translator.Loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, false, cancellationToken)).ConfigureAwait(false); + await (translator.Loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, false, cancellationToken: cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { @@ -253,7 +253,7 @@ private async Task ListUsingQueryCacheAsync(HashSet querySpaces, { log.Debug("Cache miss for multi query"); var list = await (DoListAsync(cancellationToken)).ConfigureAwait(false); - await (queryCache.PutAsync(key, new ICacheAssembler[] { assembler }, new object[] { list }, false, session, cancellationToken)).ConfigureAwait(false); + await (queryCache.PutAsync(key, combinedParameters, new ICacheAssembler[] { assembler }, new object[] { list }, session, cancellationToken)).ConfigureAwait(false); result = list; } diff --git a/src/NHibernate/Async/Impl/MultipleQueriesCacheAssembler.cs b/src/NHibernate/Async/Impl/MultipleQueriesCacheAssembler.cs index 2652dead557..051862dacc2 100644 --- a/src/NHibernate/Async/Impl/MultipleQueriesCacheAssembler.cs +++ b/src/NHibernate/Async/Impl/MultipleQueriesCacheAssembler.cs @@ -101,7 +101,7 @@ public async Task GetResultFromQueryCacheAsync(ISessionImplementor sessio if (!queryParameters.ForceCacheRefresh) { IList list = - await (queryCache.GetAsync(key, new ICacheAssembler[] {this}, queryParameters.NaturalKeyLookup, querySpaces, session, cancellationToken)).ConfigureAwait(false); + await (queryCache.GetAsync(key, queryParameters, new ICacheAssembler[] {this}, querySpaces, session, cancellationToken)).ConfigureAwait(false); //we had to wrap the query results in another list in order to save all //the queries in the same bucket, now we need to do it the other way around. if (list != null) @@ -113,4 +113,4 @@ public async Task GetResultFromQueryCacheAsync(ISessionImplementor sessio return null; } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 737fac4c89b..58af26e2c36 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -129,7 +129,7 @@ protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISession queryParameters.NamedParameters); } - await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session), cancellationToken)).ConfigureAwait(false); + await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session), cancellationToken: cancellationToken)).ConfigureAwait(false); await (session.PersistenceContext.InitializeNonLazyCollectionsAsync(cancellationToken)).ConfigureAwait(false); return result; } @@ -321,7 +321,7 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet session.Batcher.CloseCommand(st, rs); } - await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, rs, session, queryParameters.IsReadOnly(session), cancellationToken)).ConfigureAwait(false); + await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, rs, session, queryParameters.IsReadOnly(session), cancellationToken: cancellationToken)).ConfigureAwait(false); if (createSubselects) { @@ -332,7 +332,9 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet } } - internal async Task InitializeEntitiesAndCollectionsAsync(IList hydratedObjects, object resultSetId, ISessionImplementor session, bool readOnly, CancellationToken cancellationToken) + internal async Task InitializeEntitiesAndCollectionsAsync( + IList hydratedObjects, object resultSetId, ISessionImplementor session, bool readOnly, + CacheBatcher cacheBatcher = null, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ICollectionPersister[] collectionPersisters = CollectionPersisters; @@ -375,13 +377,17 @@ internal async Task InitializeEntitiesAndCollectionsAsync(IList hydratedObjects, Log.Debug("total objects hydrated: {0}", hydratedObjectsSize); } - var cacheBatcher = new CacheBatcher(session); + var ownCacheBatcher = cacheBatcher == null; + if (ownCacheBatcher) + cacheBatcher = new CacheBatcher(session); for (int i = 0; i < hydratedObjectsSize; i++) { - await (TwoPhaseLoad.InitializeEntityAsync(hydratedObjects[i], readOnly, session, pre, post, - (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); + await (TwoPhaseLoad.InitializeEntityAsync( + hydratedObjects[i], readOnly, session, pre, post, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); } - await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); + if (ownCacheBatcher) + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } if (collectionPersisters != null) @@ -1216,62 +1222,51 @@ private async Task ListUsingQueryCacheAsync(ISessionImplementor session, return GetResultList(result, queryParameters.ResultTransformer); } - internal async Task GetResultFromQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, - ISet querySpaces, IQueryCache queryCache, - QueryKey key, CancellationToken cancellationToken) + private async Task GetResultFromQueryCacheAsync( + ISessionImplementor session, QueryParameters queryParameters, ISet querySpaces, + IQueryCache queryCache, QueryKey key, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - IList result = null; + if (!CanGetFromCache(session, queryParameters)) + return null; - if (!queryParameters.ForceCacheRefresh && session.CacheMode.HasFlag(CacheMode.Get)) - { - IPersistenceContext persistenceContext = session.PersistenceContext; - - bool defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; - - if (queryParameters.IsReadOnlyInitialized) - persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; - else - queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + var result = await (queryCache.GetAsync( + key, queryParameters, + queryParameters.HasAutoDiscoverScalarTypes + ? null + : key.ResultTransformer.GetCachedResultTypes(ResultTypes), + querySpaces, session, cancellationToken)).ConfigureAwait(false); - try + if (_factory.Statistics.IsStatisticsEnabled) + { + if (result == null) { - result = await (queryCache.GetAsync( - key, - queryParameters.HasAutoDiscoverScalarTypes ? null : key.ResultTransformer.GetCachedResultTypes(ResultTypes), - queryParameters.NaturalKeyLookup, querySpaces, session, cancellationToken)).ConfigureAwait(false); - if (_factory.Statistics.IsStatisticsEnabled) - { - if (result == null) - { - _factory.StatisticsImplementor.QueryCacheMiss(QueryIdentifier, queryCache.RegionName); - } - else - { - _factory.StatisticsImplementor.QueryCacheHit(QueryIdentifier, queryCache.RegionName); - } - } + _factory.StatisticsImplementor.QueryCacheMiss(QueryIdentifier, queryCache.RegionName); } - finally + else { - persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + _factory.StatisticsImplementor.QueryCacheHit(QueryIdentifier, queryCache.RegionName); } - } + return result; } - internal async Task PutResultInQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, + private async Task PutResultInQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, IQueryCache queryCache, QueryKey key, IList result, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (session.CacheMode.HasFlag(CacheMode.Put)) + if (!session.CacheMode.HasFlag(CacheMode.Put)) + return; + + var put = await (queryCache.PutAsync( + key, queryParameters, + key.ResultTransformer.GetCachedResultTypes(ResultTypes), + result, session, cancellationToken)).ConfigureAwait(false); + + if (put && _factory.Statistics.IsStatisticsEnabled) { - bool put = await (queryCache.PutAsync(key, key.ResultTransformer.GetCachedResultTypes(ResultTypes), result, queryParameters.NaturalKeyLookup, session, cancellationToken)).ConfigureAwait(false); - if (put && _factory.Statistics.IsStatisticsEnabled) - { - _factory.StatisticsImplementor.QueryCachePut(QueryIdentifier, queryCache.RegionName); - } + _factory.StatisticsImplementor.QueryCachePut(QueryIdentifier, queryCache.RegionName); } } diff --git a/src/NHibernate/Async/Multi/CriteriaBatchItem.cs b/src/NHibernate/Async/Multi/CriteriaBatchItem.cs index 2e812ec87ee..cc67da2ffb7 100644 --- a/src/NHibernate/Async/Multi/CriteriaBatchItem.cs +++ b/src/NHibernate/Async/Multi/CriteriaBatchItem.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader.Criteria; using NHibernate.Persister.Entity; diff --git a/src/NHibernate/Async/Multi/IQueryBatchItem.cs b/src/NHibernate/Async/Multi/IQueryBatchItem.cs index 0f8ef792167..257f6673964 100644 --- a/src/NHibernate/Async/Multi/IQueryBatchItem.cs +++ b/src/NHibernate/Async/Multi/IQueryBatchItem.cs @@ -21,13 +21,6 @@ namespace NHibernate.Multi public partial interface IQueryBatchItem { - - /// - /// Get the commands to execute for getting the not-already cached results of this query. - /// - /// A cancellation token that can be used to cancel the work - /// The commands for obtaining the results not already cached. - Task> GetCommandsAsync(CancellationToken cancellationToken); /// /// Process the result sets generated by . Advance the results set @@ -36,14 +29,6 @@ public partial interface IQueryBatchItem /// The number of rows processed. Task ProcessResultsSetAsync(DbDataReader reader, CancellationToken cancellationToken); - /// - /// Process the results of the query, including cached results. - /// - /// A cancellation token that can be used to cancel the work - /// Any result from the database must have been previously processed - /// through . - Task ProcessResultsAsync(CancellationToken cancellationToken); - /// /// Execute immediately the query as a single standalone query. Used in case the data-provider /// does not support batches. diff --git a/src/NHibernate/Async/Multi/QueryBatch.cs b/src/NHibernate/Async/Multi/QueryBatch.cs index 68934aa8743..e1faf9c5b47 100644 --- a/src/NHibernate/Async/Multi/QueryBatch.cs +++ b/src/NHibernate/Async/Multi/QueryBatch.cs @@ -9,12 +9,15 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using NHibernate.Cache; using NHibernate.Driver; using NHibernate.Engine; using NHibernate.Exceptions; +using NHibernate.Type; namespace NHibernate.Multi { @@ -94,30 +97,21 @@ private async Task> GetResultsAsync(IQueryBatchItem quer return ((IQueryBatchItem) query).GetResults(); } - private async Task CombineQueriesAsync(IResultSetsCommand resultSetsCommand, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - foreach (var multiSource in _queries) - foreach (var cmd in await (multiSource.GetCommandsAsync(cancellationToken)).ConfigureAwait(false)) - { - resultSetsCommand.Append(cmd); - } - } - protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var querySpaces = new HashSet(_queries.SelectMany(t => t.GetQuerySpaces())); if (querySpaces.Count > 0) { + // The auto-flush must be handled before querying the cache, because an auto-flush may + // have to invalidate cached data, data which otherwise would cause a command to be skipped. await (Session.AutoFlushIfRequiredAsync(querySpaces, cancellationToken)).ConfigureAwait(false); } + await (GetCachedResultsAsync(cancellationToken)).ConfigureAwait(false); + var resultSetsCommand = Session.Factory.ConnectionProvider.Driver.GetResultSetsCommand(Session); - // CombineQueries queries the second level cache, which may contain stale data in regard to - // the session changes. For having them invalidated, auto-flush must have been handled before - // calling CombineQueries. - await (CombineQueriesAsync(resultSetsCommand, cancellationToken)).ConfigureAwait(false); + CombineQueries(resultSetsCommand); var statsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; Stopwatch stopWatch = null; @@ -139,26 +133,40 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) { using (var reader = await (resultSetsCommand.GetReaderAsync(Timeout, cancellationToken)).ConfigureAwait(false)) { - foreach (var multiSource in _queries) + var cacheBatcher = new CacheBatcher(Session); + foreach (var query in _queries) { - rowCount += await (multiSource.ProcessResultsSetAsync(reader, cancellationToken)).ConfigureAwait(false); + if (query.CachingInformation != null) + { + foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable)) + { + cachingInfo.SetCacheBatcher(cacheBatcher); + } + } + + rowCount += await (query.ProcessResultsSetAsync(reader, cancellationToken)).ConfigureAwait(false); } + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } } - foreach (var multiSource in _queries) + // Query cacheable results must be cached untransformed: the put does not need to wait for + // the ProcessResults. + await (PutCacheableResultsAsync(cancellationToken)).ConfigureAwait(false); + + foreach (var query in _queries) { - await (multiSource.ProcessResultsAsync(cancellationToken)).ConfigureAwait(false); + query.ProcessResults(); } } catch (OperationCanceledException) { throw; } catch (Exception sqle) { - Log.Error(sqle, "Failed to execute multi query: [{0}]", resultSetsCommand.Sql); + Log.Error(sqle, "Failed to execute query batch: [{0}]", resultSetsCommand.Sql); throw ADOExceptionHelper.Convert( Session.Factory.SQLExceptionConverter, sqle, - "Failed to execute multi query", + "Failed to execute query batch", resultSetsCommand.Sql); } @@ -166,10 +174,94 @@ protected async Task ExecuteBatchedAsync(CancellationToken cancellationToken) { stopWatch.Stop(); Session.Factory.StatisticsImplementor.QueryExecuted( - $"{_queries.Count} queries", + resultSetsCommand.Sql.ToString(), rowCount, stopWatch.Elapsed); } } + + private async Task GetCachedResultsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var statisticsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + var queriesByCaches = GetQueriesByCaches(ci => ci.CanGetFromCache); + foreach (var queriesByCache in queriesByCaches) + { + var queryInfos = queriesByCache.ToArray(); + var cache = queriesByCache.Key; + var keys = new QueryKey[queryInfos.Length]; + var parameters = new QueryParameters[queryInfos.Length]; + var returnTypes = new ICacheAssembler[queryInfos.Length][]; + var spaces = new ISet[queryInfos.Length]; + for (var i = 0; i < queryInfos.Length; i++) + { + var queryInfo = queryInfos[i]; + keys[i] = queryInfo.CacheKey; + parameters[i] = queryInfo.Parameters; + returnTypes[i] = queryInfo.Parameters.HasAutoDiscoverScalarTypes + ? null + : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + spaces[i] = queryInfo.QuerySpaces; + } + + var results = await (cache.GetManyAsync(keys, parameters, returnTypes, spaces, Session, cancellationToken)).ConfigureAwait(false); + + for (var i = 0; i < queryInfos.Length; i++) + { + queryInfos[i].SetCachedResult(results[i]); + + if (statisticsEnabled) + { + var queryIdentifier = queryInfos[i].QueryIdentifier; + if (results[i] == null) + { + Session.Factory.StatisticsImplementor.QueryCacheMiss(queryIdentifier, cache.RegionName); + } + else + { + Session.Factory.StatisticsImplementor.QueryCacheHit(queryIdentifier, cache.RegionName); + } + } + } + } + } + + private async Task PutCacheableResultsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var statisticsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + var queriesByCaches = GetQueriesByCaches(ci => ci.ResultToCache != null); + foreach (var queriesByCache in queriesByCaches) + { + var queryInfos = queriesByCache.ToArray(); + var cache = queriesByCache.Key; + var keys = new QueryKey[queryInfos.Length]; + var parameters = new QueryParameters[queryInfos.Length]; + var returnTypes = new ICacheAssembler[queryInfos.Length][]; + var results = new IList[queryInfos.Length]; + for (var i = 0; i < queryInfos.Length; i++) + { + var queryInfo = queryInfos[i]; + keys[i] = queryInfo.CacheKey; + parameters[i] = queryInfo.Parameters; + returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + results[i] = queryInfo.ResultToCache; + } + + var putted = await (cache.PutManyAsync(keys, parameters, returnTypes, results, Session, cancellationToken)).ConfigureAwait(false); + + if (!statisticsEnabled) + continue; + + for (var i = 0; i < queryInfos.Length; i++) + { + if (putted[i]) + { + Session.Factory.StatisticsImplementor.QueryCachePut( + queryInfos[i].QueryIdentifier, cache.RegionName); + } + } + } + } } } diff --git a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs index ae9650b847f..9360351debb 100644 --- a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs @@ -16,6 +16,7 @@ using NHibernate.Cache; using NHibernate.Engine; using NHibernate.SqlCommand; +using NHibernate.Type; using NHibernate.Util; namespace NHibernate.Multi @@ -25,40 +26,12 @@ namespace NHibernate.Multi public abstract partial class QueryBatchItemBase : IQueryBatchItem { - /// - public async Task> GetCommandsAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var yields = new List(); - for (var index = 0; index < _queryInfos.Count; index++) - { - var qi = _queryInfos[index]; - - if (qi.Loader.IsCacheable(qi.Parameters)) - { - qi.IsCacheable = true; - // Check if the results are available in the cache - qi.Cache = Session.Factory.GetQueryCache(qi.Parameters.CacheRegion); - qi.CacheKey = qi.Loader.GenerateQueryKey(Session, qi.Parameters); - var resultsFromCache = await (qi.Loader.GetResultFromQueryCacheAsync(Session, qi.Parameters, qi.QuerySpaces, qi.Cache, qi.CacheKey, cancellationToken)).ConfigureAwait(false); - - if (resultsFromCache != null) - { - // Cached results available, skip the command for them and stores them. - _loaderResults[index] = resultsFromCache; - qi.IsResultFromCache = true; - continue; - } - } - yields.Add(qi.Loader.CreateSqlCommand(qi.Parameters, Session)); - } - return yields; - } - /// public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + ThrowIfNotInitialized(); + var dialect = Session.Factory.Dialect; var hydratedObjects = new List[_queryInfos.Count]; @@ -126,7 +99,9 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT tmpResults.Add(o); } - _loaderResults[i] = tmpResults; + queryInfo.Result = tmpResults; + if (queryInfo.CanPutToCache) + queryInfo.ResultToCache = tmpResults; await (reader.NextResultAsync(cancellationToken)).ConfigureAwait(false); } @@ -136,39 +111,6 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT return rowCount; } - /// - public async Task ProcessResultsAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - for (var i = 0; i < _queryInfos.Count; i++) - { - var queryInfo = _queryInfos[i]; - if (_subselectResultKeys[i] != null) - { - queryInfo.Loader.CreateSubselects(_subselectResultKeys[i], queryInfo.Parameters, Session); - } - - // Handle cache if cacheable. - if (queryInfo.IsCacheable) - { - if (!queryInfo.IsResultFromCache) - { - await (queryInfo.Loader.PutResultInQueryCacheAsync( - Session, - queryInfo.Parameters, - queryInfo.Cache, - queryInfo.CacheKey, - _loaderResults[i], cancellationToken)).ConfigureAwait(false); - } - - _loaderResults[i] = - queryInfo.Loader.TransformCacheableResults( - queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, _loaderResults[i]); - } - } - AfterLoadCallback?.Invoke(GetResults()); - } - /// public async Task ExecuteNonBatchedAsync(CancellationToken cancellationToken) { @@ -188,7 +130,8 @@ private async Task InitializeEntitiesAndCollectionsAsync(DbDataReader reader, Li if (queryInfo.IsResultFromCache) continue; await (queryInfo.Loader.InitializeEntitiesAndCollectionsAsync( - hydratedObjects[i], reader, Session, Session.PersistenceContext.DefaultReadOnly, cancellationToken)).ConfigureAwait(false); + hydratedObjects[i], reader, Session, queryInfo.Parameters.IsReadOnly(Session), + queryInfo.CacheBatcher, cancellationToken)).ConfigureAwait(false); } } } diff --git a/src/NHibernate/Cache/CacheBatcher.cs b/src/NHibernate/Cache/CacheBatcher.cs index 88a6b548636..acf96431018 100644 --- a/src/NHibernate/Cache/CacheBatcher.cs +++ b/src/NHibernate/Cache/CacheBatcher.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using NHibernate.Cache.Access; +using System.Diagnostics; using NHibernate.Engine; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; @@ -10,20 +6,20 @@ namespace NHibernate.Cache { /// - /// A batcher for batching operations of , where the batch size is retrived + /// A batcher for batching operations of , where the batch size is retrieved /// from an or . /// When a different persister or a different operation is added to the batch, the current batch will be executed. /// - internal partial class CacheBatcher + public sealed partial class CacheBatcher { private CachePutBatch _putBatch; - private ISessionImplementor _session; + private readonly ISessionImplementor _session; private AbstractCacheBatch _currentBatch; private object _currentPersister; - protected static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CacheBatcher)); + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CacheBatcher)); - public CacheBatcher(ISessionImplementor session) + internal CacheBatcher(ISessionImplementor session) { _session = session; } @@ -34,7 +30,7 @@ public CacheBatcher(ISessionImplementor session) /// /// The entity persister. /// The data to put in the cache. - public void AddToBatch(IEntityPersister persister, CachePutData data) + internal void AddToBatch(IEntityPersister persister, CachePutData data) { if (ShouldExecuteBatch(persister, _putBatch)) { @@ -55,7 +51,7 @@ public void AddToBatch(IEntityPersister persister, CachePutData data) /// /// The collection persister. /// The data to put in the cache. - public void AddToBatch(ICollectionPersister persister, CachePutData data) + internal void AddToBatch(ICollectionPersister persister, CachePutData data) { if (ShouldExecuteBatch(persister, _putBatch)) { @@ -73,7 +69,7 @@ public void AddToBatch(ICollectionPersister persister, CachePutData data) /// /// Executes the current batch. /// - public void ExecuteBatch() + internal void ExecuteBatch() { if (_currentBatch == null || _currentBatch.BatchSize == 0) { @@ -102,7 +98,7 @@ public void ExecuteBatch() /// /// Cleans up the current batch. /// - public void Cleanup() + internal void Cleanup() { _putBatch = null; diff --git a/src/NHibernate/Cache/IQueryCache.cs b/src/NHibernate/Cache/IQueryCache.cs index 77874f4c2e2..f454f1c4c7f 100644 --- a/src/NHibernate/Cache/IQueryCache.cs +++ b/src/NHibernate/Cache/IQueryCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using NHibernate.Engine; @@ -7,18 +8,287 @@ namespace NHibernate.Cache { /// /// Defines the contract for caches capable of storing query results. These - /// caches should only concern themselves with storing the matching result ids. + /// caches should only concern themselves with storing the matching result ids + /// of entities. /// The transactional semantics are necessarily less strict than the semantics /// of an item cache. + /// should also be implemented for + /// compatibility with future versions. /// public partial interface IQueryCache { + /// + /// The underlying . + /// ICache Cache { get; } + + /// + /// The cache region. + /// string RegionName { get; } + /// + /// Clear the cache. + /// void Clear(); + + // Since 5.2 + [Obsolete("Have the query cache implement IBatchableQueryCache, and use IBatchableQueryCache.Put")] bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session); + + // Since 5.2 + [Obsolete("Have the query cache implement IBatchableQueryCache, and use IBatchableQueryCache.Get")] IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet spaces, ISessionImplementor session); + + /// + /// Clean up all resources. + /// void Destroy(); } + + // 6.0 TODO: merge into IQueryCache + /// + /// Transitional interface for . + /// + public partial interface IBatchableQueryCache : IQueryCache + { + /// + /// Get query results from the cache. + /// + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query spaces. + /// The session for which the query is executed. + /// The query results, if cached. + IList Get( + QueryKey key, QueryParameters queryParameters, ICacheAssembler[] returnTypes, ISet spaces, + ISessionImplementor session); + + /// + /// Put query results in the cache. + /// + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query result. + /// The session for which the query was executed. + /// if the result has been cached, + /// otherwise. + bool Put( + QueryKey key, QueryParameters queryParameters, ICacheAssembler[] returnTypes, IList result, + ISessionImplementor session); + + /// + /// Retrieve multiple query results from the cache. + /// + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query spaces matching . + /// The session for which the queries are executed. + /// The cached query results, matching each key of respectively. For each + /// missed key, it will contain a . + IList[] GetMany( + QueryKey[] keys, QueryParameters[] queryParameters, ICacheAssembler[][] returnTypes, + ISet[] spaces, ISessionImplementor session); + + /// + /// Attempt to cache objects, after loading them from the database. + /// + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query results matching . + /// The session for which the queries were executed. + /// An array of boolean indicating if each query was successfully cached. + /// + bool[] PutMany( + QueryKey[] keys, QueryParameters[] queryParameters, ICacheAssembler[][] returnTypes, IList[] results, + ISessionImplementor session); + } + + // 6.0 TODO: drop + internal static partial class QueryCacheExtensions + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(QueryCacheExtensions)); + + // Non thread safe: not an issue, at worst it will cause a few more logs than one. + // Does not handle the possibility of using multiple diffreent obsoleted query cache implementation: + // only the first encountered will be logged. + private static bool _hasWarnForObsoleteQueryCache; + + /// + /// Get query results from the cache. + /// + /// The cache. + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query spaces. + /// The session for which the query is executed. + /// The query results, if cached. + public static IList Get( + this IQueryCache queryCache, + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + ISet spaces, + ISessionImplementor session) + { + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return batchableQueryCache.Get( + key, + queryParameters, + returnTypes, + spaces, + session); + } + + if (!_hasWarnForObsoleteQueryCache) + { + _hasWarnForObsoleteQueryCache = true; + Log.Warn("{0} is obsolete, it should implement {1}", queryCache, nameof(IBatchableQueryCache)); + } + + var persistenceContext = session.PersistenceContext; + + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + + if (queryParameters.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; + else + queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + + try + { +#pragma warning disable 618 + return queryCache.Get( +#pragma warning restore 618 + key, + returnTypes, + queryParameters.NaturalKeyLookup, + spaces, + session); + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + /// + /// Put query results in the cache. + /// + /// The cache. + /// The query key. + /// The query parameters. + /// The query result row types. + /// The query result. + /// The session for which the query was executed. + /// if the result has been cached, + /// otherwise. + public static bool Put( + this IQueryCache queryCache, + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + IList result, + ISessionImplementor session) + { + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return batchableQueryCache.Put( + key, queryParameters, + returnTypes, + result, session); + } + +#pragma warning disable 618 + return queryCache.Put( +#pragma warning restore 618 + key, + returnTypes, + result, + queryParameters.NaturalKeyLookup, + session); + } + + /// + /// Retrieve multiple query results from the cache. + /// + /// The cache. + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query spaces matching . + /// The session for which the queries are executed. + /// The cached query results, matching each key of respectively. For each + /// missed key, it will contain a . + public static IList[] GetMany( + this IQueryCache queryCache, + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + ISet[] spaces, + ISessionImplementor session) + { + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return batchableQueryCache.GetMany( + keys, + queryParameters, + returnTypes, + spaces, + session); + } + + var results = new IList[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + results[i] = queryCache.Get(keys[i], queryParameters[i], returnTypes[i], spaces[i], session); + } + + return results; + } + + /// + /// Attempt to cache objects, after loading them from the database. + /// + /// The cache. + /// The query keys. + /// The array of query parameters matching . + /// The array of query result row types matching . + /// The array of query results matching . + /// The session for which the queries were executed. + /// An array of boolean indicating if each query was successfully cached. + /// + public static bool[] PutMany( + this IQueryCache queryCache, + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + IList[] results, + ISessionImplementor session) + { + if (queryCache is IBatchableQueryCache batchableQueryCache) + { + return batchableQueryCache.PutMany( + keys, + queryParameters, + returnTypes, + results, + session); + } + + var puts = new bool[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + puts[i] = queryCache.Put(keys[i], queryParameters[i], returnTypes[i], results[i], session); + } + + return puts; + } + } } diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index a1643d0f998..453101bc13c 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; - +using System.Linq; using NHibernate.Cfg; using NHibernate.Engine; using NHibernate.Type; @@ -15,10 +15,12 @@ namespace NHibernate.Cache /// results and re-running queries when it detects this condition, recaching /// the new results. /// - public partial class StandardQueryCache : IQueryCache + public partial class StandardQueryCache : IQueryCache, IBatchableQueryCache { private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof (StandardQueryCache)); private readonly ICache _queryCache; + private readonly IBatchableReadOnlyCache _batchableReadOnlyCache; + private readonly IBatchableCache _batchableCache; private readonly string _regionName; private readonly UpdateTimestampsCache _updateTimestampsCache; @@ -34,6 +36,8 @@ public StandardQueryCache(Settings settings, IDictionary props, Log.Info("starting query cache at region: {0}", regionName); _queryCache = settings.CacheProvider.BuildCache(regionName, props); + _batchableReadOnlyCache = _queryCache as IBatchableReadOnlyCache; + _batchableCache = _queryCache as IBatchableCache; _updateTimestampsCache = updateTimestampsCache; _regionName = regionName; } @@ -55,34 +59,67 @@ public void Clear() _queryCache.Clear(); } + /// + public bool Put( + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + IList result, + ISessionImplementor session) + { + // 6.0 TODO: inline the call. +#pragma warning disable 612 + return Put(key, returnTypes, result, queryParameters.NaturalKeyLookup, session); +#pragma warning restore 612 + } + + // Since 5.2 + [Obsolete] public bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool isNaturalKeyLookup, ISessionImplementor session) { if (isNaturalKeyLookup && result.Count == 0) return false; - long ts = session.Factory.Settings.CacheProvider.NextTimestamp(); + var ts = session.Factory.Settings.CacheProvider.NextTimestamp(); - if (Log.IsDebugEnabled()) - Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); + Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); - IList cacheable = new List(result.Count + 1) {ts}; - for (int i = 0; i < result.Count; i++) - { - if (returnTypes.Length == 1) - { - cacheable.Add(returnTypes[0].Disassemble(result[i], session, null)); - } - else - { - cacheable.Add(TypeHelper.Disassemble((object[]) result[i], returnTypes, null, session, null)); - } - } - - _queryCache.Put(key, cacheable); + _queryCache.Put(key, GetCacheableResult(returnTypes, session, result, ts)); return true; } + /// + public IList Get( + QueryKey key, + QueryParameters queryParameters, + ICacheAssembler[] returnTypes, + ISet spaces, + ISessionImplementor session) + { + var persistenceContext = session.PersistenceContext; + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + + if (queryParameters.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; + else + queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + + try + { + // 6.0 TODO: inline the call. +#pragma warning disable 612 + return Get(key, returnTypes, queryParameters.NaturalKeyLookup, spaces, session); +#pragma warning restore 612 + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + // Since 5.2 + [Obsolete] public IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyLookup, ISet spaces, ISessionImplementor session) { if (Log.IsDebugEnabled()) @@ -106,60 +143,252 @@ public IList Get(QueryKey key, ICacheAssembler[] returnTypes, bool isNaturalKeyL return null; } - Log.Debug("returning cached query results for: {0}", key); - if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) + return GetResultFromCacheable(key, returnTypes, isNaturalKeyLookup, session, cacheable); + } + + /// + public bool[] PutMany( + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + IList[] results, + ISessionImplementor session) + { + var cached = new bool[keys.Length]; + if (_batchableCache == null) { - returnTypes = GuessTypes(cacheable); + for (var i = 0; i < keys.Length; i++) + { + cached[i] = Put(keys[i], queryParameters[i], returnTypes[i], results[i], session); + } + return cached; } - for (int i = 1; i < cacheable.Count; i++) + var ts = session.Factory.Settings.CacheProvider.NextTimestamp(); + + if (Log.IsDebugEnabled()) + Log.Debug("caching query results in region: '{0}'; {1}", _regionName, StringHelper.CollectionToString(keys)); + + var cachedKeys = new List(); + var cachedResults = new List(); + for (var i = 0; i < keys.Length; i++) + { + var result = results[i]; + if (queryParameters[i].NaturalKeyLookup && result.Count == 0) + continue; + + cached[i] = true; + cachedKeys.Add(keys[i]); + cachedResults.Add(GetCacheableResult(returnTypes[i], session, result, ts)); + } + + _batchableCache.PutMany(cachedKeys.ToArray(), cachedResults.ToArray()); + + return cached; + } + + /// + public IList[] GetMany( + QueryKey[] keys, + QueryParameters[] queryParameters, + ICacheAssembler[][] returnTypes, + ISet[] spaces, + ISessionImplementor session) + { + var results = new IList[keys.Length]; + if (_batchableReadOnlyCache == null) + { + for (var i = 0; i < keys.Length; i++) + { + results[i] = Get(keys[i], queryParameters[i], returnTypes[i], spaces[i], session); + } + return results; + } + + if (Log.IsDebugEnabled()) + Log.Debug("checking cached query results in region: '{0}'; {1}", _regionName, StringHelper.CollectionToString(keys)); + + var cacheables = _batchableReadOnlyCache.GetMany(keys).Cast().ToArray(); + + var spacesToCheck = new List>(); + var checkedSpacesIndexes = new HashSet(); + var checkedSpacesTimestamp = new List(); + for (var i = 0; i < keys.Length; i++) + { + var cacheable = cacheables[i]; + if (cacheable == null) + { + Log.Debug("query results were not found in cache: {0}", keys[i]); + continue; + } + + 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]); + if (Log.IsDebugEnabled()) + Log.Debug("Checking query spaces for up-to-dateness [{0}]", StringHelper.CollectionToString(querySpaces)); + } + + var upToDates = spacesToCheck.Count > 0 + ? _updateTimestampsCache.AreUpToDate(spacesToCheck.ToArray(), checkedSpacesTimestamp.ToArray()) + : Array.Empty(); + + var upToDatesIndex = 0; + var persistenceContext = session.PersistenceContext; + var defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + for (var i = 0; i < keys.Length; i++) + { + var cacheable = cacheables[i]; + if (cacheable == null) + continue; + + var key = keys[i]; + if (checkedSpacesIndexes.Contains(i) && !upToDates[upToDatesIndex++]) + { + Log.Debug("cached query results were not up to date for: {0}", key); + continue; + } + + var queryParams = queryParameters[i]; + if (queryParams.IsReadOnlyInitialized) + persistenceContext.DefaultReadOnly = queryParams.ReadOnly; + else + queryParams.ReadOnly = persistenceContext.DefaultReadOnly; + + try + { + results[i] = GetResultFromCacheable(key, returnTypes[i], queryParams.NaturalKeyLookup, session, cacheable); + } + finally + { + persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + } + } + + return results; + } + + public void Destroy() + { + try + { + _queryCache.Destroy(); + } + catch (Exception e) + { + Log.Warn(e, "could not destroy query cache: {0}", _regionName); + } + } + + #endregion + + private static List GetCacheableResult( + ICacheAssembler[] returnTypes, + ISessionImplementor session, + IList result, + long ts) + { + var cacheable = new List(result.Count + 1) { ts }; + foreach (var row in result) { if (returnTypes.Length == 1) { - returnTypes[0]?.BeforeAssemble(cacheable[i], session); + cacheable.Add(returnTypes[0].Disassemble(row, session, null)); } else { - TypeHelper.BeforeAssemble((object[])cacheable[i], returnTypes, session); + cacheable.Add(TypeHelper.Disassemble((object[])row, returnTypes, null, session, null)); } } - IList result = new List(cacheable.Count - 1); - for (int i = 1; i < cacheable.Count; i++) + return cacheable; + } + + private IList GetResultFromCacheable( + QueryKey key, + ICacheAssembler[] returnTypes, + bool isNaturalKeyLookup, + ISessionImplementor session, + IList cacheable) + { + Log.Debug("returning cached query results for: {0}", key); + if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) { - try + returnTypes = GuessTypes(cacheable); + } + + try + { + var result = new List(cacheable.Count - 1); + if (returnTypes.Length == 1) { - if (returnTypes.Length == 1) + 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++) { - result.Add(returnTypes[0].Assemble(cacheable[i], session, null)); + rows.Add(cacheable[i]); } - else + + foreach (var row in rows) { - result.Add(TypeHelper.Assemble((object[])cacheable[i], returnTypes, session, null)); + returnType.BeforeAssemble(row, session); + } + + foreach (var row in rows) + { + result.Add(returnType.Assemble(row, session, null)); } } - catch (UnresolvableObjectException ex) + else { - if (isNaturalKeyLookup) + // Skip first element, it is the timestamp + var rows = new List(cacheable.Count - 1); + for (var i = 1; i < cacheable.Count; i++) { - //TODO: not really completely correct, since - // the UnresolvableObjectException could occur while resolving - // associations, leaving the PC in an inconsistent state - Log.Debug(ex, "could not reassemble cached result set"); - _queryCache.Remove(key); - return null; + rows.Add((object[]) cacheable[i]); } - throw; + foreach (var row in rows) + { + TypeHelper.BeforeAssemble(row, returnTypes, session); + } + + foreach (var row in rows) + { + result.Add(TypeHelper.Assemble(row, returnTypes, session, null)); + } } + + return result; } + catch (UnresolvableObjectException ex) + { + if (isNaturalKeyLookup) + { + //TODO: not really completely correct, since + // the UnresolvableObjectException could occur while resolving + // associations, leaving the PC in an inconsistent state + Log.Debug(ex, "could not reassemble cached result set"); + // Handling a RemoveMany here does not look worth it, as this case short-circuits + // the result-set. So a Many could only benefit batched queries, and only if many + // of them are natural key lookup with an unresolvable object case. + _queryCache.Remove(key); + return null; + } - return result; + throw; + } } private static ICacheAssembler[] GuessTypes(IList cacheable) { - var firstRow = cacheable[0]; var colCount = (cacheable[0] as object[])?.Length ?? 1; var returnTypes = new ICacheAssembler[colCount]; if (colCount == 1) @@ -199,20 +428,6 @@ private static ICacheAssembler[] GuessTypes(IList cacheable) return returnTypes; } - public void Destroy() - { - try - { - _queryCache.Destroy(); - } - catch (Exception e) - { - Log.Warn(e, "could not destroy query cache: {0}", _regionName); - } - } - - #endregion - protected virtual bool IsUpToDate(ISet spaces, long timestamp) { return _updateTimestampsCache.IsUpToDate(spaces, timestamp); diff --git a/src/NHibernate/Cache/UpdateTimestampsCache.cs b/src/NHibernate/Cache/UpdateTimestampsCache.cs index 8641bbdab1b..ae9973aca9e 100644 --- a/src/NHibernate/Cache/UpdateTimestampsCache.cs +++ b/src/NHibernate/Cache/UpdateTimestampsCache.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using NHibernate.Cfg; +using NHibernate.Util; namespace NHibernate.Cache { @@ -18,7 +19,8 @@ public partial class UpdateTimestampsCache { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(UpdateTimestampsCache)); private ICache updateTimestamps; - private readonly IBatchableReadOnlyCache _batchUpdateTimestamps; + private readonly IBatchableReadOnlyCache _batchReadOnlyUpdateTimestamps; + private readonly IBatchableCache _batchUpdateTimestamps; private readonly string regionName = typeof(UpdateTimestampsCache).Name; @@ -34,7 +36,8 @@ public UpdateTimestampsCache(Settings settings, IDictionary prop log.Info("starting update timestamps cache at region: {0}", regionName); updateTimestamps = settings.CacheProvider.BuildCache(regionName, props); // ReSharper disable once SuspiciousTypeConversion.Global - _batchUpdateTimestamps = updateTimestamps as IBatchableReadOnlyCache; + _batchReadOnlyUpdateTimestamps = updateTimestamps as IBatchableReadOnlyCache; + _batchUpdateTimestamps = updateTimestamps as IBatchableCache; } //Since v5.1 @@ -49,11 +52,8 @@ public void PreInvalidate(object[] spaces) public virtual void PreInvalidate(IReadOnlyCollection spaces) { //TODO: to handle concurrent writes correctly, this should return a Lock to the client - long ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; - foreach (var space in spaces) - { - updateTimestamps.Put(space, ts); - } + var ts = updateTimestamps.NextTimestamp() + updateTimestamps.Timeout; + SetSpacesTimestamp(spaces, ts); //TODO: return new Lock(ts); } @@ -72,45 +72,107 @@ public virtual void Invalidate(IReadOnlyCollection spaces) //TODO: to handle concurrent writes correctly, the client should pass in a Lock long ts = updateTimestamps.NextTimestamp(); //TODO: if lock.getTimestamp().equals(ts) - foreach (var space in spaces) + if (log.IsDebugEnabled()) + log.Debug("Invalidating spaces [{0}]", StringHelper.CollectionToString(spaces)); + SetSpacesTimestamp(spaces, ts); + } + + private void SetSpacesTimestamp(IReadOnlyCollection spaces, long ts) + { + if (_batchUpdateTimestamps != null) { - log.Debug("Invalidating space [{0}]", space); - updateTimestamps.Put(space, ts); + if (spaces.Count == 0) + return; + + var timestamps = new object[spaces.Count]; + for (var i = 0; i < timestamps.Length; i++) + { + timestamps[i] = ts; + } + + _batchUpdateTimestamps.PutMany(spaces.ToArray(), timestamps); + } + else + { + foreach (var space in spaces) + { + updateTimestamps.Put(space, ts); + } } } [MethodImpl(MethodImplOptions.Synchronized)] public virtual bool IsUpToDate(ISet spaces, long timestamp /* H2.1 has Long here */) { - if (_batchUpdateTimestamps != null) + if (_batchReadOnlyUpdateTimestamps != null) { + if (spaces.Count == 0) + return true; + var keys = new object[spaces.Count]; var index = 0; foreach (var space in spaces) { keys[index++] = space; } - var lastUpdates = _batchUpdateTimestamps.GetMany(keys); - foreach (var lastUpdate in lastUpdates) + var lastUpdates = _batchReadOnlyUpdateTimestamps.GetMany(keys); + return lastUpdates.All(lastUpdate => !IsOutdated(lastUpdate as long?, timestamp)); + } + + return spaces.Select(space => updateTimestamps.Get(space)) + .All(lastUpdate => !IsOutdated(lastUpdate as long?, timestamp)); + } + + [MethodImpl(MethodImplOptions.Synchronized)] + public virtual bool[] AreUpToDate(ISet[] spaces, long[] timestamps) + { + var results = new bool[spaces.Length]; + var allSpaces = new HashSet(); + foreach (var sp in spaces) + { + allSpaces.UnionWith(sp); + } + + if (_batchReadOnlyUpdateTimestamps != null) + { + if (allSpaces.Count == 0) { - if (IsOutdated(lastUpdate, timestamp)) + for (var i = 0; i < spaces.Length; i++) { - return false; + results[i] = true; } + + return results; } - return true; - } - foreach (string space in spaces) + var keys = new object[allSpaces.Count]; + var index = 0; + foreach (var space in allSpaces) + { + keys[index++] = space; + } + + index = 0; + var lastUpdatesBySpace = + _batchReadOnlyUpdateTimestamps + .GetMany(keys) + .ToDictionary(u => keys[index++], u => u as long?); + + for (var i = 0; i < spaces.Length; i++) + { + var timestamp = timestamps[i]; + results[i] = spaces[i].All(space => !IsOutdated(lastUpdatesBySpace[space], timestamp)); + } + } + else { - object lastUpdate = updateTimestamps.Get(space); - if (IsOutdated(lastUpdate, timestamp)) + for (var i = 0; i < spaces.Length; i++) { - return false; + results[i] = IsUpToDate(spaces[i], timestamps[i]); } - } - return true; + + return results; } public virtual void Destroy() @@ -125,9 +187,9 @@ public virtual void Destroy() } } - private bool IsOutdated(object lastUpdate, long timestamp) + private bool IsOutdated(long? lastUpdate, long timestamp) { - if (lastUpdate == null) + if (!lastUpdate.HasValue) { //the last update timestamp was lost from the cache //(or there were no updates since startup!) @@ -148,7 +210,7 @@ private bool IsOutdated(object lastUpdate, long timestamp) } else { - if ((long) lastUpdate >= timestamp) + if (lastUpdate >= timestamp) { return true; } diff --git a/src/NHibernate/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Impl/MultiCriteriaImpl.cs index 021aeb05a0d..3c289010fbf 100644 --- a/src/NHibernate/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Impl/MultiCriteriaImpl.cs @@ -144,7 +144,7 @@ private IList ListUsingQueryCache(HashSet querySpaces) result = list; if (session.CacheMode.HasFlag(CacheMode.Put)) { - bool put = queryCache.Put(key, new ICacheAssembler[] { assembler }, new object[] { list }, combinedParameters.NaturalKeyLookup, session); + bool put = queryCache.Put(key, combinedParameters, new ICacheAssembler[] { assembler }, new object[] { list }, session); if (put && factory.Statistics.IsStatisticsEnabled) { factory.StatisticsImplementor.QueryCachePut(key.ToString(), queryCache.RegionName); diff --git a/src/NHibernate/Impl/MultiQueryImpl.cs b/src/NHibernate/Impl/MultiQueryImpl.cs index 0fd9869b62e..67410ee220e 100644 --- a/src/NHibernate/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Impl/MultiQueryImpl.cs @@ -726,7 +726,7 @@ private IList ListUsingQueryCache(HashSet querySpaces) { log.Debug("Cache miss for multi query"); var list = DoList(); - queryCache.Put(key, new ICacheAssembler[] { assembler }, new object[] { list }, false, session); + queryCache.Put(key, combinedParameters, new ICacheAssembler[] { assembler }, new object[] { list }, session); result = list; } diff --git a/src/NHibernate/Impl/MultipleQueriesCacheAssembler.cs b/src/NHibernate/Impl/MultipleQueriesCacheAssembler.cs index 4d013246e58..51503c508e8 100644 --- a/src/NHibernate/Impl/MultipleQueriesCacheAssembler.cs +++ b/src/NHibernate/Impl/MultipleQueriesCacheAssembler.cs @@ -77,7 +77,7 @@ public IList GetResultFromQueryCache(ISessionImplementor session, QueryParameter if (!queryParameters.ForceCacheRefresh) { IList list = - queryCache.Get(key, new ICacheAssembler[] {this}, queryParameters.NaturalKeyLookup, querySpaces, session); + queryCache.Get(key, queryParameters, new ICacheAssembler[] {this}, querySpaces, session); //we had to wrap the query results in another list in order to save all //the queries in the same bucket, now we need to do it the other way around. if (list != null) @@ -89,4 +89,4 @@ public IList GetResultFromQueryCache(ISessionImplementor session, QueryParameter return null; } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 137e943aaba..c63bd9fbcbf 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -599,7 +599,9 @@ private IEnumerable CreateSubselects(IList keys, Qu } } - internal void InitializeEntitiesAndCollections(IList hydratedObjects, object resultSetId, ISessionImplementor session, bool readOnly) + internal void InitializeEntitiesAndCollections( + IList hydratedObjects, object resultSetId, ISessionImplementor session, bool readOnly, + CacheBatcher cacheBatcher = null) { ICollectionPersister[] collectionPersisters = CollectionPersisters; if (collectionPersisters != null) @@ -641,13 +643,17 @@ internal void InitializeEntitiesAndCollections(IList hydratedObjects, object res Log.Debug("total objects hydrated: {0}", hydratedObjectsSize); } - var cacheBatcher = new CacheBatcher(session); + var ownCacheBatcher = cacheBatcher == null; + if (ownCacheBatcher) + cacheBatcher = new CacheBatcher(session); for (int i = 0; i < hydratedObjectsSize; i++) { - TwoPhaseLoad.InitializeEntity(hydratedObjects[i], readOnly, session, pre, post, - (persister, data) => cacheBatcher.AddToBatch(persister, data)); + TwoPhaseLoad.InitializeEntity( + hydratedObjects[i], readOnly, session, pre, post, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); } - cacheBatcher.ExecuteBatch(); + if (ownCacheBatcher) + cacheBatcher.ExecuteBatch(); } if (collectionPersisters != null) @@ -1708,60 +1714,54 @@ private CacheableResultTransformer CreateCacheableResultTransformer(QueryParamet queryParameters.HasAutoDiscoverScalarTypes, SqlString); } - internal IList GetResultFromQueryCache(ISessionImplementor session, QueryParameters queryParameters, - ISet querySpaces, IQueryCache queryCache, - QueryKey key) + internal bool CanGetFromCache(ISessionImplementor session, QueryParameters queryParameters) { - IList result = null; - - if (!queryParameters.ForceCacheRefresh && session.CacheMode.HasFlag(CacheMode.Get)) - { - IPersistenceContext persistenceContext = session.PersistenceContext; + return !queryParameters.ForceCacheRefresh && session.CacheMode.HasFlag(CacheMode.Get); + } - bool defaultReadOnlyOrig = persistenceContext.DefaultReadOnly; + private IList GetResultFromQueryCache( + ISessionImplementor session, QueryParameters queryParameters, ISet querySpaces, + IQueryCache queryCache, QueryKey key) + { + if (!CanGetFromCache(session, queryParameters)) + return null; - if (queryParameters.IsReadOnlyInitialized) - persistenceContext.DefaultReadOnly = queryParameters.ReadOnly; - else - queryParameters.ReadOnly = persistenceContext.DefaultReadOnly; + var result = queryCache.Get( + key, queryParameters, + queryParameters.HasAutoDiscoverScalarTypes + ? null + : key.ResultTransformer.GetCachedResultTypes(ResultTypes), + querySpaces, session); - try + if (_factory.Statistics.IsStatisticsEnabled) + { + if (result == null) { - result = queryCache.Get( - key, - queryParameters.HasAutoDiscoverScalarTypes ? null : key.ResultTransformer.GetCachedResultTypes(ResultTypes), - queryParameters.NaturalKeyLookup, querySpaces, session); - if (_factory.Statistics.IsStatisticsEnabled) - { - if (result == null) - { - _factory.StatisticsImplementor.QueryCacheMiss(QueryIdentifier, queryCache.RegionName); - } - else - { - _factory.StatisticsImplementor.QueryCacheHit(QueryIdentifier, queryCache.RegionName); - } - } + _factory.StatisticsImplementor.QueryCacheMiss(QueryIdentifier, queryCache.RegionName); } - finally + else { - persistenceContext.DefaultReadOnly = defaultReadOnlyOrig; + _factory.StatisticsImplementor.QueryCacheHit(QueryIdentifier, queryCache.RegionName); } - } + return result; } - internal void PutResultInQueryCache(ISessionImplementor session, QueryParameters queryParameters, + private void PutResultInQueryCache(ISessionImplementor session, QueryParameters queryParameters, IQueryCache queryCache, QueryKey key, IList result) { - if (session.CacheMode.HasFlag(CacheMode.Put)) + if (!session.CacheMode.HasFlag(CacheMode.Put)) + return; + + var put = queryCache.Put( + key, queryParameters, + key.ResultTransformer.GetCachedResultTypes(ResultTypes), + result, session); + + if (put && _factory.Statistics.IsStatisticsEnabled) { - bool put = queryCache.Put(key, key.ResultTransformer.GetCachedResultTypes(ResultTypes), result, queryParameters.NaturalKeyLookup, session); - if (put && _factory.Statistics.IsStatisticsEnabled) - { - _factory.StatisticsImplementor.QueryCachePut(QueryIdentifier, queryCache.RegionName); - } + _factory.StatisticsImplementor.QueryCachePut(QueryIdentifier, queryCache.RegionName); } } diff --git a/src/NHibernate/Multi/CriteriaBatchItem.cs b/src/NHibernate/Multi/CriteriaBatchItem.cs index 306ffa59454..6c6e6bfc150 100644 --- a/src/NHibernate/Multi/CriteriaBatchItem.cs +++ b/src/NHibernate/Multi/CriteriaBatchItem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader.Criteria; using NHibernate.Persister.Entity; @@ -15,7 +16,7 @@ public CriteriaBatchItem(ICriteria query) _criteria = (CriteriaImpl) query ?? throw new ArgumentNullException(nameof(query)); } - protected override List GetQueryLoadInfo() + protected override List GetQueryInformation(ISessionImplementor session) { var factory = Session.Factory; //for detached criteria @@ -24,7 +25,7 @@ protected override List GetQueryLoadInfo() string[] implementors = factory.GetImplementors(_criteria.EntityOrClassName); int size = implementors.Length; - var list = new List(size); + var list = new List(size); for (int i = 0; i < size; i++) { CriteriaLoader loader = new CriteriaLoader( @@ -35,13 +36,7 @@ protected override List GetQueryLoadInfo() Session.EnabledFilters ); - list.Add( - new QueryLoadInfo() - { - Loader = loader, - Parameters = loader.Translator.GetQueryParameters(), - QuerySpaces = loader.QuerySpaces, - }); + list.Add(new QueryInfo(loader.Translator.GetQueryParameters(), loader, loader.QuerySpaces, session)); } return list; diff --git a/src/NHibernate/Multi/ICachingInformation.cs b/src/NHibernate/Multi/ICachingInformation.cs new file mode 100644 index 00000000000..906d79ac5ee --- /dev/null +++ b/src/NHibernate/Multi/ICachingInformation.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using NHibernate.Cache; +using NHibernate.Engine; +using NHibernate.Type; + +namespace NHibernate.Multi +{ + /// + /// Querying information. + /// + public interface ICachingInformation + { + /// + /// Is the query cacheable? + /// + bool IsCacheable { get; } + + /// + /// The query cache key. + /// + QueryKey CacheKey { get; } + + /// + /// The query parameters. + /// + QueryParameters Parameters { get; } + + /// + /// The query spaces. + /// + /// + /// Query spaces indicates which entity classes are used by the query and need to be flushed + /// when auto-flush is enabled. It also indicates which cache update timestamps needs to be + /// checked for up-to-date-ness. + /// + ISet QuerySpaces { get; } + + /// + /// Can the query be obtained from cache? + /// + bool CanGetFromCache { get; } + + /// + /// The query result types. + /// + IType[] ResultTypes { get; } + + /// + /// The query result to put in the cache. if no put should be done. + /// + IList ResultToCache { get; } + + /// + /// The query identifier, for statistics purpose. + /// + string QueryIdentifier { get; } + + /// + /// Set the result retrieved from the cache. + /// + /// The results. Can be in case of cache miss. + void SetCachedResult(IList result); + + /// + /// Set the to use for batching entities and collections cache puts. + /// + /// A cache batcher. + void SetCacheBatcher(CacheBatcher cacheBatcher); + } +} diff --git a/src/NHibernate/Multi/IQueryBatchItem.cs b/src/NHibernate/Multi/IQueryBatchItem.cs index 2bceb711108..6e69f1d15f1 100644 --- a/src/NHibernate/Multi/IQueryBatchItem.cs +++ b/src/NHibernate/Multi/IQueryBatchItem.cs @@ -29,6 +29,14 @@ public interface IQueryBatchItem : IQueryBatchItem /// public partial interface IQueryBatchItem { + /// + /// Optionally, the query caching information list, for batching. Each element matches + /// a SQL-Query resulting from the query translation, in the order they are translated. + /// It should yield an empty enumerable if no batching of caching is handled for this + /// query. + /// + IEnumerable CachingInformation { get; } + /// /// Initialize the query. Method is called right before batch execution. /// Can be used for various delayed initialization logic. diff --git a/src/NHibernate/Multi/QueryBatch.cs b/src/NHibernate/Multi/QueryBatch.cs index 1720294d6b1..ff25cd5076e 100644 --- a/src/NHibernate/Multi/QueryBatch.cs +++ b/src/NHibernate/Multi/QueryBatch.cs @@ -1,10 +1,13 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using NHibernate.Cache; using NHibernate.Driver; using NHibernate.Engine; using NHibernate.Exceptions; +using NHibernate.Type; namespace NHibernate.Multi { @@ -121,27 +124,19 @@ private void Init() } } - private void CombineQueries(IResultSetsCommand resultSetsCommand) - { - foreach (var multiSource in _queries) - foreach (var cmd in multiSource.GetCommands()) - { - resultSetsCommand.Append(cmd); - } - } - protected void ExecuteBatched() { var querySpaces = new HashSet(_queries.SelectMany(t => t.GetQuerySpaces())); if (querySpaces.Count > 0) { + // The auto-flush must be handled before querying the cache, because an auto-flush may + // have to invalidate cached data, data which otherwise would cause a command to be skipped. Session.AutoFlushIfRequired(querySpaces); } + GetCachedResults(); + var resultSetsCommand = Session.Factory.ConnectionProvider.Driver.GetResultSetsCommand(Session); - // CombineQueries queries the second level cache, which may contain stale data in regard to - // the session changes. For having them invalidated, auto-flush must have been handled before - // calling CombineQueries. CombineQueries(resultSetsCommand); var statsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; @@ -164,25 +159,39 @@ protected void ExecuteBatched() { using (var reader = resultSetsCommand.GetReader(Timeout)) { - foreach (var multiSource in _queries) + var cacheBatcher = new CacheBatcher(Session); + foreach (var query in _queries) { - rowCount += multiSource.ProcessResultsSet(reader); + if (query.CachingInformation != null) + { + foreach (var cachingInfo in query.CachingInformation.Where(ci => ci.IsCacheable)) + { + cachingInfo.SetCacheBatcher(cacheBatcher); + } + } + + rowCount += query.ProcessResultsSet(reader); } + cacheBatcher.ExecuteBatch(); } } - foreach (var multiSource in _queries) + // Query cacheable results must be cached untransformed: the put does not need to wait for + // the ProcessResults. + PutCacheableResults(); + + foreach (var query in _queries) { - multiSource.ProcessResults(); + query.ProcessResults(); } } catch (Exception sqle) { - Log.Error(sqle, "Failed to execute multi query: [{0}]", resultSetsCommand.Sql); + Log.Error(sqle, "Failed to execute query batch: [{0}]", resultSetsCommand.Sql); throw ADOExceptionHelper.Convert( Session.Factory.SQLExceptionConverter, sqle, - "Failed to execute multi query", + "Failed to execute query batch", resultSetsCommand.Sql); } @@ -190,10 +199,112 @@ protected void ExecuteBatched() { stopWatch.Stop(); Session.Factory.StatisticsImplementor.QueryExecuted( - $"{_queries.Count} queries", + resultSetsCommand.Sql.ToString(), rowCount, stopWatch.Elapsed); } } + + private void GetCachedResults() + { + var statisticsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + var queriesByCaches = GetQueriesByCaches(ci => ci.CanGetFromCache); + foreach (var queriesByCache in queriesByCaches) + { + var queryInfos = queriesByCache.ToArray(); + var cache = queriesByCache.Key; + var keys = new QueryKey[queryInfos.Length]; + var parameters = new QueryParameters[queryInfos.Length]; + var returnTypes = new ICacheAssembler[queryInfos.Length][]; + var spaces = new ISet[queryInfos.Length]; + for (var i = 0; i < queryInfos.Length; i++) + { + var queryInfo = queryInfos[i]; + keys[i] = queryInfo.CacheKey; + parameters[i] = queryInfo.Parameters; + returnTypes[i] = queryInfo.Parameters.HasAutoDiscoverScalarTypes + ? null + : queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + spaces[i] = queryInfo.QuerySpaces; + } + + var results = cache.GetMany(keys, parameters, returnTypes, spaces, Session); + + for (var i = 0; i < queryInfos.Length; i++) + { + queryInfos[i].SetCachedResult(results[i]); + + if (statisticsEnabled) + { + var queryIdentifier = queryInfos[i].QueryIdentifier; + if (results[i] == null) + { + Session.Factory.StatisticsImplementor.QueryCacheMiss(queryIdentifier, cache.RegionName); + } + else + { + Session.Factory.StatisticsImplementor.QueryCacheHit(queryIdentifier, cache.RegionName); + } + } + } + } + } + + private void CombineQueries(IResultSetsCommand resultSetsCommand) + { + foreach (var query in _queries) + foreach (var cmd in query.GetCommands()) + { + resultSetsCommand.Append(cmd); + } + } + + private void PutCacheableResults() + { + var statisticsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + var queriesByCaches = GetQueriesByCaches(ci => ci.ResultToCache != null); + foreach (var queriesByCache in queriesByCaches) + { + var queryInfos = queriesByCache.ToArray(); + var cache = queriesByCache.Key; + var keys = new QueryKey[queryInfos.Length]; + var parameters = new QueryParameters[queryInfos.Length]; + var returnTypes = new ICacheAssembler[queryInfos.Length][]; + var results = new IList[queryInfos.Length]; + for (var i = 0; i < queryInfos.Length; i++) + { + var queryInfo = queryInfos[i]; + keys[i] = queryInfo.CacheKey; + parameters[i] = queryInfo.Parameters; + returnTypes[i] = queryInfo.CacheKey.ResultTransformer.GetCachedResultTypes(queryInfo.ResultTypes); + results[i] = queryInfo.ResultToCache; + } + + var putted = cache.PutMany(keys, parameters, returnTypes, results, Session); + + if (!statisticsEnabled) + continue; + + for (var i = 0; i < queryInfos.Length; i++) + { + if (putted[i]) + { + Session.Factory.StatisticsImplementor.QueryCachePut( + queryInfos[i].QueryIdentifier, cache.RegionName); + } + } + } + } + + private IEnumerable> GetQueriesByCaches(Func cachingInformationFilter) + { + return + _queries + .Where(q => q.CachingInformation != null) + .SelectMany(q => q.CachingInformation) + .Where(ci => ci != null && cachingInformationFilter(ci)) + .GroupBy( + ci => Session.Factory.GetQueryCache(ci.Parameters.CacheRegion)); + } } } diff --git a/src/NHibernate/Multi/QueryBatchItem.cs b/src/NHibernate/Multi/QueryBatchItem.cs index 5a2569199f4..71de6a2a4ae 100644 --- a/src/NHibernate/Multi/QueryBatchItem.cs +++ b/src/NHibernate/Multi/QueryBatchItem.cs @@ -15,19 +15,17 @@ public QueryBatchItem(IQuery query) Query = (AbstractQueryImpl) query ?? throw new ArgumentNullException(nameof(query)); } - protected override List GetQueryLoadInfo() + protected override List GetQueryInformation(ISessionImplementor session) { Query.VerifyParameters(); QueryParameters queryParameters = Query.GetQueryParameters(); queryParameters.ValidateParameters(); - return Query.GetTranslators(Session, queryParameters).Select( - t => new QueryLoadInfo() - { - Loader = t.Loader, - Parameters = queryParameters, - QuerySpaces = new HashSet(t.QuerySpaces), - }).ToList(); + return + Query + .GetTranslators(Session, queryParameters) + .Select(t => new QueryInfo(queryParameters, t.Loader, new HashSet(t.QuerySpaces), session)) + .ToList(); } protected override IList GetResultsNonBatched() diff --git a/src/NHibernate/Multi/QueryBatchItemBase.cs b/src/NHibernate/Multi/QueryBatchItemBase.cs index 1e0abe7439d..89407c582a2 100644 --- a/src/NHibernate/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Multi/QueryBatchItemBase.cs @@ -6,6 +6,7 @@ using NHibernate.Cache; using NHibernate.Engine; using NHibernate.SqlCommand; +using NHibernate.Type; using NHibernate.Util; namespace NHibernate.Multi @@ -17,36 +18,127 @@ public abstract partial class QueryBatchItemBase : IQueryBatchItem[] _subselectResultKeys; - private IList[] _loaderResults; - - private List _queryInfos; + private List _queryInfos; private IList _finalResults; - protected class QueryLoadInfo + protected class QueryInfo : ICachingInformation { - public Loader.Loader Loader; - public QueryParameters Parameters; - + /// + /// The query loader. + /// + public Loader.Loader Loader { get; set; } + + /// + /// The query result. + /// + public IList Result { get; set; } + + /// + public QueryParameters Parameters { get; } + + /// + public ISet QuerySpaces { get; } + //Cache related properties: - public bool IsCacheable; - public ISet QuerySpaces; - public IQueryCache Cache; - public QueryKey CacheKey; - public bool IsResultFromCache; + + /// + public bool IsCacheable { get; } + + /// + public QueryKey CacheKey { get;} + + /// + public bool CanGetFromCache { get; } + + // Do not store but forward instead: Loader.ResultTypes can be null initially (if AutoDiscoverTypes + // is enabled). + /// + public IType[] ResultTypes => Loader.ResultTypes; + + /// + public string QueryIdentifier => Loader.QueryIdentifier; + + /// + public IList ResultToCache { get; set; } + + /// + /// Indicates if the query result was obtained from the cache. + /// + public bool IsResultFromCache { get; private set; } + + /// + /// Should a result retrieved from database be cached? + /// + public bool CanPutToCache { get; } + + /// + /// The cache batcher to use for entities and collections puts. + /// + public CacheBatcher CacheBatcher { get; private set; } + + /// + /// Create a new QueryInfo. + /// + /// The query parameters. + /// The loader. + /// The query spaces. + /// The session of the query. + public QueryInfo( + QueryParameters parameters, Loader.Loader loader, ISet querySpaces, + ISessionImplementor session) + { + Parameters = parameters; + Loader = loader; + QuerySpaces = querySpaces; + + IsCacheable = loader.IsCacheable(parameters); + if (!IsCacheable) + return; + + CacheKey = Loader.GenerateQueryKey(session, Parameters); + CanGetFromCache = Loader.CanGetFromCache(session, Parameters); + CanPutToCache = session.CacheMode.HasFlag(CacheMode.Put); + } + + /// + public void SetCachedResult(IList result) + { + if (!IsCacheable) + throw new InvalidOperationException("Cannot set cached result on a non cacheable query"); + if (Result != null) + throw new InvalidOperationException("Result is already set"); + Result = result; + IsResultFromCache = result != null; + } + + /// + public void SetCacheBatcher(CacheBatcher cacheBatcher) + { + CacheBatcher = cacheBatcher; + } } - protected abstract List GetQueryLoadInfo(); + protected abstract List GetQueryInformation(ISessionImplementor session); + + /// + public IEnumerable CachingInformation + { + get + { + ThrowIfNotInitialized(); + return _queryInfos; + } + } /// public virtual void Init(ISessionImplementor session) { Session = session; - _queryInfos = GetQueryLoadInfo(); + _queryInfos = GetQueryInformation(session); var count = _queryInfos.Count; _subselectResultKeys = new List[count]; - _loaderResults = new IList[count]; _finalResults = null; } @@ -60,26 +152,12 @@ public IEnumerable GetQuerySpaces() /// public IEnumerable GetCommands() { - for (var index = 0; index < _queryInfos.Count; index++) - { - var qi = _queryInfos[index]; - - if (qi.Loader.IsCacheable(qi.Parameters)) - { - qi.IsCacheable = true; - // Check if the results are available in the cache - qi.Cache = Session.Factory.GetQueryCache(qi.Parameters.CacheRegion); - qi.CacheKey = qi.Loader.GenerateQueryKey(Session, qi.Parameters); - var resultsFromCache = qi.Loader.GetResultFromQueryCache(Session, qi.Parameters, qi.QuerySpaces, qi.Cache, qi.CacheKey); + ThrowIfNotInitialized(); - if (resultsFromCache != null) - { - // Cached results available, skip the command for them and stores them. - _loaderResults[index] = resultsFromCache; - qi.IsResultFromCache = true; - continue; - } - } + foreach (var qi in _queryInfos) + { + if (qi.IsResultFromCache) + continue; yield return qi.Loader.CreateSqlCommand(qi.Parameters, Session); } @@ -88,6 +166,8 @@ public IEnumerable GetCommands() /// public int ProcessResultsSet(DbDataReader reader) { + ThrowIfNotInitialized(); + var dialect = Session.Factory.Dialect; var hydratedObjects = new List[_queryInfos.Count]; @@ -155,7 +235,9 @@ public int ProcessResultsSet(DbDataReader reader) tmpResults.Add(o); } - _loaderResults[i] = tmpResults; + queryInfo.Result = tmpResults; + if (queryInfo.CanPutToCache) + queryInfo.ResultToCache = tmpResults; reader.NextResult(); } @@ -168,6 +250,8 @@ public int ProcessResultsSet(DbDataReader reader) /// public void ProcessResults() { + ThrowIfNotInitialized(); + for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; @@ -176,22 +260,12 @@ public void ProcessResults() queryInfo.Loader.CreateSubselects(_subselectResultKeys[i], queryInfo.Parameters, Session); } - // Handle cache if cacheable. if (queryInfo.IsCacheable) { - if (!queryInfo.IsResultFromCache) - { - queryInfo.Loader.PutResultInQueryCache( - Session, - queryInfo.Parameters, - queryInfo.Cache, - queryInfo.CacheKey, - _loaderResults[i]); - } - - _loaderResults[i] = + // This transformation must not be applied to ResultToCache. + queryInfo.Result = queryInfo.Loader.TransformCacheableResults( - queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, _loaderResults[i]); + queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, queryInfo.Result); } } AfterLoadCallback?.Invoke(GetResults()); @@ -208,16 +282,17 @@ public void ExecuteNonBatched() protected List GetTypedResults() { - if (_loaderResults == null) + ThrowIfNotInitialized(); + if (_queryInfos.Any(qi => qi.Result == null)) { - throw new HibernateException("Batch wasn't executed. You must call IQueryBatch.Execute() before accessing results."); + throw new InvalidOperationException("Some query results are missing, batch is likely not fully executed yet."); } - var results = new List(_loaderResults.Sum(tr => tr.Count)); - for (var i = 0; i < _queryInfos.Count; i++) + var results = new List(_queryInfos.Sum(qi => qi.Result.Count)); + foreach (var queryInfo in _queryInfos) { - var list = _queryInfos[i].Loader.GetResultList( - _loaderResults[i], - _queryInfos[i].Parameters.ResultTransformer); + var list = queryInfo.Loader.GetResultList( + queryInfo.Result, + queryInfo.Parameters.ResultTransformer); ArrayHelper.AddAll(results, list); } @@ -243,8 +318,19 @@ private void InitializeEntitiesAndCollections(DbDataReader reader, List[ if (queryInfo.IsResultFromCache) continue; queryInfo.Loader.InitializeEntitiesAndCollections( - hydratedObjects[i], reader, Session, Session.PersistenceContext.DefaultReadOnly); + hydratedObjects[i], reader, Session, queryInfo.Parameters.IsReadOnly(Session), + queryInfo.CacheBatcher); } } + + private void ThrowIfNotInitialized() + { + if (_queryInfos == null) + throw new InvalidOperationException( + "The query item has not been initialized. A query item must belong to a batch " + + $"({nameof(IQueryBatch)}) and the batch must be executed ({nameof(IQueryBatch)}." + + $"{nameof(IQueryBatch.Execute)} or {nameof(IQueryBatch)}.{nameof(IQueryBatch.GetResult)}) " + + "before retrieving the item result."); + } } }