From 33e300dedce891ff99c1eeefa572dc13aaadaa02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:36:04 +0200 Subject: [PATCH] Support caching queries with autodiscovered types fix #3169 --- .../SqlTest/Query/NativeSQLQueriesFixture.cs | 119 ++++++++++-------- src/NHibernate/Cache/QueryAliasesKey.cs | 39 ++++++ src/NHibernate/Cache/StandardQueryCache.cs | 68 +++++++++- src/NHibernate/Loader/Loader.cs | 3 +- .../Transform/CacheableResultTransformer.cs | 7 ++ 5 files changed, 178 insertions(+), 58 deletions(-) create mode 100644 src/NHibernate/Cache/QueryAliasesKey.cs diff --git a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs index 73845ae91c7..d7f7fb16009 100644 --- a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -1,8 +1,9 @@ using System.Collections; using System.Linq; +using NHibernate.Criterion; +using NHibernate.Multi; using NHibernate.Transform; using NUnit.Framework; -using NHibernate.Criterion; namespace NHibernate.Test.SqlTest.Query { @@ -51,6 +52,20 @@ protected override string MappingsAssembly get { return "NHibernate.Test"; } } + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Employment").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + Sfi.QueryCache.Clear(); + } + [Test] public void FailOnNoAddEntityOrScalar() { @@ -125,18 +140,6 @@ public void SQLQueryInterface() t.Commit(); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - - t.Commit(); - s.Close(); - } } [Test] @@ -191,18 +194,6 @@ public void SQLQueryInterfaceCacheable() t.Commit(); s.Close(); } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - - t.Commit(); - s.Close(); - } } [Test(Description = "GH-2904")] @@ -252,20 +243,11 @@ IList GetCacheableSqlQueryResults() Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "results are expected from cache"); } } - - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) - { - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - t.Commit(); - } } class ResultDto { + public long orgId { get; set; } public string regionCode { get; set; } } @@ -294,27 +276,67 @@ void AssertQuery(bool fromCache) .List(); t.Commit(); - Assert.AreEqual(1, l.Count); - //TODO: Uncomment if we properly fix caching auto discovery type queries with transformers - // var msg = "results are expected from " + (fromCache ? "cache" : "DB"); - // Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); - // Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); + Assert.That(l.Count, Is.EqualTo(1)); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 1), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 1 : 0), msg); } } - AssertQuery(false); AssertQuery(true); + } - using (var s = OpenSession()) - using (var t = s.BeginTransaction()) + [Test(Description = "GH-3169")] + public void CacheableScalarSQLMultiQueryWithTransformer() + { + Organization ifa = new Organization("IFA"); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) { - s.Delete(ifa); + s.Save(ifa); t.Commit(); } + + void AssertQuery(bool fromCache) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + using (EnableStatisticsScope()) + { + var q1 = s.CreateSQLQuery("select org.NAME as regionCode from ORGANIZATION org") + .AddScalar("regionCode", NHibernateUtil.String) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + var q2 = s.CreateSQLQuery("select org.ORGID as orgId from ORGANIZATION org") + .AddScalar("orgId", NHibernateUtil.Int64) + .SetResultTransformer(Transformers.AliasToBean()) + .SetCacheable(true); + + var batch = s.CreateQueryBatch(); + batch.Add(q1); + batch.Add(q2); + batch.Execute(); + + var l1 = batch.GetResult(0); + var l2 = batch.GetResult(1); + + t.Commit(); + + Assert.That(l1.Count, Is.EqualTo(1), "Unexpected results count for the first query."); + Assert.That(l2.Count, Is.EqualTo(1), "Unexpected results count for the second query."); + var msg = "Results are expected from " + (fromCache ? "cache" : "DB"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(fromCache ? 0 : 2), msg); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(fromCache ? 2 : 0), msg); + } + } + + AssertQuery(false); + AssertQuery(true); } - [Test] + [Test] public void ResultSetMappingDefinition() { ISession s = OpenSession(); @@ -339,11 +361,6 @@ public void ResultSetMappingDefinition() .List(); Assert.AreEqual(l.Count, 1); - s.Delete(emp); - s.Delete(gavin); - s.Delete(ifa); - s.Delete(jboss); - t.Commit(); s.Close(); } @@ -426,8 +443,6 @@ public void ScalarValues() Assert.AreEqual(o[1], "JBoss"); Assert.AreEqual(o[0], idJBoss); - s.Delete(ifa); - s.Delete(jboss); t.Commit(); s.Close(); } diff --git a/src/NHibernate/Cache/QueryAliasesKey.cs b/src/NHibernate/Cache/QueryAliasesKey.cs new file mode 100644 index 00000000000..7f1aad00c27 --- /dev/null +++ b/src/NHibernate/Cache/QueryAliasesKey.cs @@ -0,0 +1,39 @@ +using System; + +namespace NHibernate.Cache +{ + [Serializable] + public class QueryAliasesKey : IEquatable + { + private readonly QueryKey _queryKey; + + /// + /// Initializes a new instance of the class. + /// + /// The of the query for which aliases have to be stored. + public QueryAliasesKey(QueryKey queryKey) + { + _queryKey = queryKey ?? throw new ArgumentNullException(nameof(queryKey)); + } + + public override bool Equals(object other) + { + return Equals(other as QueryAliasesKey); + } + + public bool Equals(QueryAliasesKey other) + { + return other != null && _queryKey.Equals(other._queryKey); + } + + public override int GetHashCode() + { + return _queryKey.GetHashCode(); + } + + public override string ToString() + { + return "QueryAlisesKey: " + _queryKey.ToString(); + } + } +} diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index 8cda99f3815..5b869d123a1 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using NHibernate.Cfg; -using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -86,8 +85,15 @@ public bool Put( { // 6.0 TODO: inline the call. #pragma warning disable 612 - return Put(key, returnTypes, result, queryParameters.NaturalKeyLookup, session); + var cached = Put(key, returnTypes, result, queryParameters.NaturalKeyLookup, session); #pragma warning restore 612 + + if (cached && key.ResultTransformer?.AutoDiscoverTypes == true && key.ResultTransformer.AutoDiscoveredAliases != null) + { + Cache.Put(new QueryAliasesKey(key), key.ResultTransformer.AutoDiscoveredAliases); + } + + return cached; } // Since 5.2 @@ -126,8 +132,22 @@ public IList Get( { // 6.0 TODO: inline the call. #pragma warning disable 612 - return Get(key, returnTypes, queryParameters.NaturalKeyLookup, spaces, session); + var result = Get(key, returnTypes, queryParameters.NaturalKeyLookup, spaces, session); #pragma warning restore 612 + + if (result != null && key.ResultTransformer?.AutoDiscoverTypes == true && result.Count > 0) + { + var aliasesKey = new QueryAliasesKey(key); + if (!(Cache.Get(aliasesKey) is string[] aliases)) + { + // Cannot properly initialize the result transformer, treat it as a cache miss + Log.Debug("query aliases were not found in cache: {0}", aliasesKey); + return null; + } + key.ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters.ResultTransformer, aliases); + } + + return result; } finally { @@ -184,9 +204,16 @@ public bool[] PutMany( if (queryParameters[i].NaturalKeyLookup && result.Count == 0) continue; + var key = keys[i]; cached[i] = true; - cachedKeys.Add(keys[i]); + cachedKeys.Add(key); cachedResults.Add(GetCacheableResult(returnTypes[i], session, result, ts)); + + if (key.ResultTransformer?.AutoDiscoverTypes == true && key.ResultTransformer.AutoDiscoveredAliases != null) + { + cachedKeys.Add(new QueryAliasesKey(key)); + cachedResults.Add(key.ResultTransformer.AutoDiscoveredAliases); + } } _cache.PutMany(cachedKeys.ToArray(), cachedResults.ToArray()); @@ -244,6 +271,8 @@ public IList[] GetMany( { session.PersistenceContext.BatchFetchQueue.InitializeQueryCacheQueue(); + var queryAliasesKeys = new QueryAliasesKey[keys.Length]; + var hasAliasesToFetch = false; for (var i = 0; i < keys.Length; i++) { var cacheable = (IList) cacheables[i]; @@ -267,6 +296,37 @@ public IList[] GetMany( finalReturnTypes[i] = GetReturnTypes(key, returnTypes[i], cacheable); PerformBeforeAssemble(finalReturnTypes[i], session, cacheable); + + if (key.ResultTransformer?.AutoDiscoverTypes == true && cacheable.Count > 0) + { + queryAliasesKeys[i] = new QueryAliasesKey(key); + hasAliasesToFetch = true; + } + } + + if (hasAliasesToFetch) + { + var allAliases = _cache.GetMany(queryAliasesKeys.Where(k => k != null).ToArray()); + + var aliasesIndex = 0; + for (var i = 0; i < keys.Length; i++) + { + var queryAliasesKey = queryAliasesKeys[i]; + if (queryAliasesKey == null) + continue; + + if (allAliases[aliasesIndex] is string[] aliases) + { + keys[i].ResultTransformer.SupplyAutoDiscoveredParameters(queryParameters[i].ResultTransformer, aliases); + } + else + { + // Cannot properly initialize the result transformer, treat it as a cache miss + Log.Debug("query aliases were not found in cache: {0}", queryAliasesKey); + finalReturnTypes[i] = null; + } + aliasesIndex++; + } } for (var i = 0; i < keys.Length; i++) diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index ba56146dbbd..8069cc9a544 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1829,8 +1829,7 @@ protected IList List(ISessionImplementor session, QueryParameters queryParameter internal bool IsCacheable(QueryParameters queryParameters) { - return _factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable - && !(queryParameters.HasAutoDiscoverScalarTypes && queryParameters.ResultTransformer != null); + return _factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable; } private IList ListIgnoreQueryCache(ISessionImplementor session, QueryParameters queryParameters) diff --git a/src/NHibernate/Transform/CacheableResultTransformer.cs b/src/NHibernate/Transform/CacheableResultTransformer.cs index e3dba66d028..3f732f20165 100644 --- a/src/NHibernate/Transform/CacheableResultTransformer.cs +++ b/src/NHibernate/Transform/CacheableResultTransformer.cs @@ -23,6 +23,11 @@ public class CacheableResultTransformer : IResultTransformer public bool AutoDiscoverTypes { get; } + /// + /// The auto-discovered aliaises. + /// + public string[] AutoDiscoveredAliases { get; private set; } + private readonly SqlString _autoDiscoveredQuery; private readonly bool _skipTransformer; private int _tupleLength; @@ -202,6 +207,8 @@ internal void SupplyAutoDiscoveredParameters(IResultTransformer transformer, str throw new InvalidOperationException( "Cannot supply auto-discovered parameters when it is not enabled on the transformer."); + AutoDiscoveredAliases = aliases; + var includeInTuple = ArrayHelper.Fill(true, aliases?.Length ?? 0); InitializeTransformer(includeInTuple, GetIncludeInTransform(transformer, aliases, includeInTuple)); }