diff --git a/src/NHibernate.DomainModel/Northwind/Entities/Northwind.cs b/src/NHibernate.DomainModel/Northwind/Entities/Northwind.cs index 5da2e35cadb..c4cbda23f26 100755 --- a/src/NHibernate.DomainModel/Northwind/Entities/Northwind.cs +++ b/src/NHibernate.DomainModel/Northwind/Entities/Northwind.cs @@ -94,9 +94,9 @@ public IQueryable Role get { return _session.Query(); } } - public IEnumerable IUsers + public IQueryable IUsers { get { return _session.Query(); } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.DomainModel/Northwind/Entities/NorthwindQueryOver.cs b/src/NHibernate.DomainModel/Northwind/Entities/NorthwindQueryOver.cs new file mode 100644 index 00000000000..0910e58ce99 --- /dev/null +++ b/src/NHibernate.DomainModel/Northwind/Entities/NorthwindQueryOver.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Linq; + +namespace NHibernate.DomainModel.Northwind.Entities +{ + public class NorthwindQueryOver + { + private readonly ISession _session; + + public NorthwindQueryOver(ISession session) + { + _session = session; + } + + public IQueryOver Customers + { + get { return _session.QueryOver(); } + } + + public IQueryOver Products + { + get { return _session.QueryOver(); } + } + + public IQueryOver Shippers + { + get { return _session.QueryOver(); } + } + + public IQueryOver Orders + { + get { return _session.QueryOver(); } + } + + public IQueryOver OrderLines + { + get { return _session.QueryOver(); } + } + + public IQueryOver Employees + { + get { return _session.QueryOver(); } + } + + public IQueryOver Categories + { + get { return _session.QueryOver(); } + } + + public IQueryOver Timesheets + { + get { return _session.QueryOver(); } + } + + public IQueryOver Animals + { + get { return _session.QueryOver(); } + } + + public IQueryOver Mammals + { + get { return _session.QueryOver(); } + } + + public IQueryOver Users + { + get { return _session.QueryOver(); } + } + + public IQueryOver PatientRecords + { + get { return _session.QueryOver(); } + } + + public IQueryOver States + { + get { return _session.QueryOver(); } + } + + public IQueryOver Patients + { + get { return _session.QueryOver(); } + } + + public IQueryOver Physicians + { + get { return _session.QueryOver(); } + } + + public IQueryOver Role + { + get { return _session.QueryOver(); } + } + + public IQueryOver IUsers + { + get { return _session.QueryOver(); } + } + } +} diff --git a/src/NHibernate.Test/Async/Criteria/ReadonlyTests/QueryOverCacheableTests.cs b/src/NHibernate.Test/Async/Criteria/ReadonlyTests/QueryOverCacheableTests.cs new file mode 100644 index 00000000000..b3f6e51b233 --- /dev/null +++ b/src/NHibernate.Test/Async/Criteria/ReadonlyTests/QueryOverCacheableTests.cs @@ -0,0 +1,290 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Linq; +using NHibernate.Cfg; +using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.Criteria.ReadonlyTests +{ + using System.Threading.Tasks; + [TestFixture] + public class QueryOverCacheableTestsAsync : CriteriaNorthwindReadonlyTestCase + { + //Just for discoverability + private class CriteriaCacheableTest{} + + protected override void Configure(Configuration config) + { + config.SetProperty(Environment.UseQueryCache, "true"); + config.SetProperty(Environment.GenerateStatistics, "true"); + base.Configure(config); + } + + [Test] + public async Task QueryIsCacheableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + await (db.Customers.Cacheable().Take(1).ListAsync()); + await (db.Customers.Cacheable().Take(1).ListAsync()); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task QueryIsCacheable2Async() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + await (db.Customers.Cacheable().Take(1).ListAsync()); + await (db.Customers.Take(1).ListAsync()); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "Unexpected cache hit count"); + } + + [Test] + public async Task QueryIsCacheableWithRegionAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + await (Sfi.EvictQueriesAsync("test")); + await (Sfi.EvictQueriesAsync("other")); + + await (db.Customers.Cacheable().Take(1).CacheRegion("test").ListAsync()); + await (db.Customers.Cacheable().Take(1).CacheRegion("test").ListAsync()); + await (db.Customers.Cacheable().Take(1).CacheRegion("other").ListAsync()); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task CanBeCombinedWithFetchAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + await (db.Customers + .Cacheable() + .ListAsync()); + + await (db.Orders + .Cacheable() + .Take(1) + .ListAsync()); + + await (db.Customers + .Fetch(SelectMode.Fetch, x => x.Orders) + .Cacheable() + .Take(1) + .ListAsync()); + + await (db.Orders + .Fetch(SelectMode.Fetch, x => x.OrderLines) + .Cacheable() + .Take(1) + .ListAsync()); + + await (db.Customers + .Fetch(SelectMode.Fetch, x => x.Address) + .Where(x => x.CustomerId == "VINET") + .Cacheable() + .SingleOrDefaultAsync()); + + var customer = await (db.Customers + .Fetch(SelectMode.Fetch, x => x.Address) + .Where(x => x.CustomerId == "VINET") + .Cacheable() + .SingleOrDefaultAsync()); + + Assert.That(NHibernateUtil.IsInitialized(customer.Address), Is.True, "Expected the fetched Address to be initialized"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(5), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task FetchIsCacheableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + var order = (await (db.Orders + .Fetch( + SelectMode.Fetch, + x => x.Customer, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .ListAsync())) + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + order = (await (db.Orders + .Fetch( + SelectMode.Fetch, + x => x.Customer, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .ListAsync())) + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task FetchIsCacheableForJoinAliasAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + Customer customer = null; + OrderLine orderLines = null; + Product product = null; + OrderLine prOrderLines = null; + + var order = (await (db.Orders + .JoinAlias(x => x.Customer, () => customer) + .JoinAlias(x => x.OrderLines, () => orderLines, JoinType.LeftOuterJoin) + .JoinAlias(() => orderLines.Product, () => product) + .JoinAlias(() => product.OrderLines, () => prOrderLines, JoinType.LeftOuterJoin) + .Where(x => x.OrderId == 10248) + .Cacheable() + .ListAsync())) + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + order = (await (db.Orders + .JoinAlias(x => x.Customer, () => customer) + .JoinAlias(x => x.OrderLines, () => orderLines, JoinType.LeftOuterJoin) + .JoinAlias(() => orderLines.Product, () => product) + .JoinAlias(() => product.OrderLines, () => prOrderLines, JoinType.LeftOuterJoin) + .Where(x => x.OrderId == 10248) + .Cacheable() + .ListAsync())) + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task FutureFetchIsCacheableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + db.Orders + .Fetch(SelectMode.Fetch, x => x.Customer) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future(); + + var order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future() + .ToList() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + db.Orders + .Fetch(SelectMode.Fetch, x => x.Customer) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future(); + + order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future() + .ToList() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); + } + + private static void AssertFetchedOrder(Order order) + { + Assert.That(order.Customer, Is.Not.Null, "Expected the fetched Customer to be not null"); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); + var orderLine = order.OrderLines.First(); + Assert.That(orderLine.Product, Is.Not.Null, "Expected the fetched Product to be not null"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + } + } +} diff --git a/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs b/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs index 4abcd24c0af..54a8a6cfc44 100644 --- a/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs +++ b/src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs @@ -12,6 +12,7 @@ using NHibernate.Cfg; using NHibernate.DomainModel.Northwind.Entities; using NHibernate.Linq; +using NHibernate.Transform; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -406,13 +407,102 @@ public async Task FutureFetchIsCachableAsync() Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); } + + [Explicit("Not working. dto.Customer retrieved from cache as uninitialized proxy")] + [Test] + public async Task ProjectedEntitiesAreCachableAsync() + { + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + var dto = await (session.Query() + .WithOptions(o => o.SetCacheable(true)) + .Where(x => x.OrderId == 10248) + .Select(x => new { x.Customer, Order = x }) + .FirstOrDefaultAsync()); + + Assert.That(dto, Is.Not.Null, "dto should not be null"); + Assert.That(dto.Order, Is.Not.Null, "dto.Order should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Order), Is.True, "dto.Order should be initialized"); + Assert.That(dto.Customer, Is.Not.Null, "dto.Customer should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Customer), Is.True, "dto.Customer from cache should be initialized"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + session.Clear(); + + dto = await (session.Query() + .WithOptions(o => o.SetCacheable(true)) + .Where(x => x.OrderId == 10248) + .Select(x => new { x.Customer, Order = x }) + .FirstOrDefaultAsync()); + + Assert.That(dto, Is.Not.Null, "dto from cache should not be null"); + Assert.That(dto.Order, Is.Not.Null, "dto.Order from cache should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Order), Is.True, "dto.Order from cache should be initialized"); + Assert.That(dto.Customer, Is.Not.Null, "dto.Customer from cache should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Customer), Is.True, "dto.Customer from cache should be initialized"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public async Task CacheHqlQueryWithFetchAndTransformerThatChangeTupleAsync() + { + if (!TestDialect.SupportsDuplicatedColumnAliases) + Assert.Ignore("Ignored due to GH-2092"); + + Sfi.Statistics.Clear(); + await (Sfi.EvictQueriesAsync()); + + // the combination of query and transformer doesn't make sense. + // It's simply used as example of returned data being transformed before caching leading to mismatch between + // Loader.ResultTypes collection and provided tuple + var order = await (session.CreateQuery("select o.Employee.FirstName, o from Order o join fetch o.Customer where o.OrderId = :id") + .SetInt32("id", 10248) + .SetCacheable(true) + .SetResultTransformer(Transformers.RootEntity) + .UniqueResultAsync()); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + Assert.That(order, Is.Not.Null); + Assert.That(order.Customer, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True); + + session.Clear(); + Sfi.Statistics.Clear(); + + order = await (session.CreateQuery("select o.Employee.FirstName, o from Order o join fetch o.Customer where o.OrderId = :id") + .SetInt32("id", 10248) + .SetCacheable(true) + .SetResultTransformer(Transformers.RootEntity) + .UniqueResultAsync()); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + Assert.That(order, Is.Not.Null); + Assert.That(order.Customer, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True); + } private static void AssertFetchedOrder(Order order) { + Assert.That(NHibernateUtil.IsInitialized(order), "Expected the order to be initialized"); + Assert.That(order.Customer, Is.Not.Null, "Expected the fetched Customer to be not null"); Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); var orderLine = order.OrderLines.First(); + Assert.That(orderLine.Product, Is.Not.Null, "Expected the fetched Product to be not null"); Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); } diff --git a/src/NHibernate.Test/Criteria/ReadonlyTests/CriteriaNorthwindReadonlyTestCase.cs b/src/NHibernate.Test/Criteria/ReadonlyTests/CriteriaNorthwindReadonlyTestCase.cs new file mode 100644 index 00000000000..cd8cfd50fba --- /dev/null +++ b/src/NHibernate.Test/Criteria/ReadonlyTests/CriteriaNorthwindReadonlyTestCase.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.DomainModel.Northwind.Entities; +using NUnit.Framework; + +namespace NHibernate.Test.Criteria.ReadonlyTests +{ + public abstract class CriteriaNorthwindReadonlyTestCase : NHibernate.Test.Linq.ReadonlyTestCase + { + private ISession _session = null; + protected NorthwindQueryOver db; + + protected override string[] Mappings + { + get + { + return new[] + { + "Northwind.Mappings.Customer.hbm.xml", + "Northwind.Mappings.Employee.hbm.xml", + "Northwind.Mappings.Order.hbm.xml", + "Northwind.Mappings.OrderLine.hbm.xml", + "Northwind.Mappings.Product.hbm.xml", + "Northwind.Mappings.ProductCategory.hbm.xml", + "Northwind.Mappings.Region.hbm.xml", + "Northwind.Mappings.Shipper.hbm.xml", + "Northwind.Mappings.Supplier.hbm.xml", + "Northwind.Mappings.Territory.hbm.xml", + "Northwind.Mappings.AnotherEntity.hbm.xml", + "Northwind.Mappings.Role.hbm.xml", + "Northwind.Mappings.User.hbm.xml", + "Northwind.Mappings.TimeSheet.hbm.xml", + "Northwind.Mappings.Animal.hbm.xml", + "Northwind.Mappings.Patient.hbm.xml" + }; + } + } + + public ISession Session + { + get { return _session; } + } + + protected override void OnSetUp() + { + _session = OpenSession(); + db = new NorthwindQueryOver(_session); + base.OnSetUp(); + } + + protected override void OnTearDown() + { + if (_session.IsOpen) + { + _session.Close(); + } + } + + public static void AssertByIds(IEnumerable entities, TId[] expectedIds, Converter entityIdGetter) + { + Assert.That(entities.Select(x => entityIdGetter(x)), Is.EquivalentTo(expectedIds)); + } + + protected IQueryOver Customers => Session.QueryOver(); + } +} diff --git a/src/NHibernate.Test/Criteria/ReadonlyTests/QueryOverCacheableTests.cs b/src/NHibernate.Test/Criteria/ReadonlyTests/QueryOverCacheableTests.cs new file mode 100644 index 00000000000..749a5184354 --- /dev/null +++ b/src/NHibernate.Test/Criteria/ReadonlyTests/QueryOverCacheableTests.cs @@ -0,0 +1,279 @@ +using System.Linq; +using NHibernate.Cfg; +using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.Criteria.ReadonlyTests +{ + [TestFixture] + public class QueryOverCacheableTests : CriteriaNorthwindReadonlyTestCase + { + //Just for discoverability + private class CriteriaCacheableTest{} + + protected override void Configure(Configuration config) + { + config.SetProperty(Environment.UseQueryCache, "true"); + config.SetProperty(Environment.GenerateStatistics, "true"); + base.Configure(config); + } + + [Test] + public void QueryIsCacheable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + db.Customers.Cacheable().Take(1).List(); + db.Customers.Cacheable().Take(1).List(); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void QueryIsCacheable2() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + db.Customers.Cacheable().Take(1).List(); + db.Customers.Take(1).List(); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0), "Unexpected cache hit count"); + } + + [Test] + public void QueryIsCacheableWithRegion() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + Sfi.EvictQueries("test"); + Sfi.EvictQueries("other"); + + db.Customers.Cacheable().Take(1).CacheRegion("test").List(); + db.Customers.Cacheable().Take(1).CacheRegion("test").List(); + db.Customers.Cacheable().Take(1).CacheRegion("other").List(); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void CanBeCombinedWithFetch() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + db.Customers + .Cacheable() + .List(); + + db.Orders + .Cacheable() + .Take(1) + .List(); + + db.Customers + .Fetch(SelectMode.Fetch, x => x.Orders) + .Cacheable() + .Take(1) + .List(); + + db.Orders + .Fetch(SelectMode.Fetch, x => x.OrderLines) + .Cacheable() + .Take(1) + .List(); + + db.Customers + .Fetch(SelectMode.Fetch, x => x.Address) + .Where(x => x.CustomerId == "VINET") + .Cacheable() + .SingleOrDefault(); + + var customer = db.Customers + .Fetch(SelectMode.Fetch, x => x.Address) + .Where(x => x.CustomerId == "VINET") + .Cacheable() + .SingleOrDefault(); + + Assert.That(NHibernateUtil.IsInitialized(customer.Address), Is.True, "Expected the fetched Address to be initialized"); + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(5), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void FetchIsCacheable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + var order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.Customer, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .List() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.Customer, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .List() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void FetchIsCacheableForJoinAlias() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + Customer customer = null; + OrderLine orderLines = null; + Product product = null; + OrderLine prOrderLines = null; + + var order = db.Orders + .JoinAlias(x => x.Customer, () => customer) + .JoinAlias(x => x.OrderLines, () => orderLines, JoinType.LeftOuterJoin) + .JoinAlias(() => orderLines.Product, () => product) + .JoinAlias(() => product.OrderLines, () => prOrderLines, JoinType.LeftOuterJoin) + .Where(x => x.OrderId == 10248) + .Cacheable() + .List() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + order = db.Orders + .JoinAlias(x => x.Customer, () => customer) + .JoinAlias(x => x.OrderLines, () => orderLines, JoinType.LeftOuterJoin) + .JoinAlias(() => orderLines.Product, () => product) + .JoinAlias(() => product.OrderLines, () => prOrderLines, JoinType.LeftOuterJoin) + .Where(x => x.OrderId == 10248) + .Cacheable() + .List() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void FutureFetchIsCacheable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + db.Orders + .Fetch(SelectMode.Fetch, x => x.Customer) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future(); + + var order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future() + .ToList() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + Session.Clear(); + + db.Orders + .Fetch(SelectMode.Fetch, x => x.Customer) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future(); + + order = db.Orders + .Fetch( + SelectMode.Fetch, + x => x.OrderLines, + x => x.OrderLines.First().Product, + x => x.OrderLines.First().Product.OrderLines) + .Where(x => x.OrderId == 10248) + .Cacheable() + .Future() + .ToList() + .First(); + + AssertFetchedOrder(order); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); + } + + private static void AssertFetchedOrder(Order order) + { + Assert.That(order.Customer, Is.Not.Null, "Expected the fetched Customer to be not null"); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); + var orderLine = order.OrderLines.First(); + Assert.That(orderLine.Product, Is.Not.Null, "Expected the fetched Product to be not null"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); + Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); + } + } +} diff --git a/src/NHibernate.Test/Criteria/ReadonlyTests/ReadonlyFixtureSetUp.cs b/src/NHibernate.Test/Criteria/ReadonlyTests/ReadonlyFixtureSetUp.cs new file mode 100644 index 00000000000..b2ba33908d6 --- /dev/null +++ b/src/NHibernate.Test/Criteria/ReadonlyTests/ReadonlyFixtureSetUp.cs @@ -0,0 +1,12 @@ +using NUnit.Framework; + +namespace NHibernate.Test.Criteria.ReadonlyTests +{ + /// + /// Single one-time fixture set up for all test fixtures in NHibernate.Test.Criteria.ReadonlyTests namespace + /// + [SetUpFixture] + public class ReadonlyFixtureSetUp : NHibernate.Test.Linq.LinqReadonlyTestsContext + { + } +} diff --git a/src/NHibernate.Test/Linq/QueryCacheableTests.cs b/src/NHibernate.Test/Linq/QueryCacheableTests.cs index 79aedf77c5f..4475971f09e 100644 --- a/src/NHibernate.Test/Linq/QueryCacheableTests.cs +++ b/src/NHibernate.Test/Linq/QueryCacheableTests.cs @@ -2,6 +2,7 @@ using NHibernate.Cfg; using NHibernate.DomainModel.Northwind.Entities; using NHibernate.Linq; +using NHibernate.Transform; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -395,13 +396,102 @@ public void FutureFetchIsCachable() Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count"); } + + [Explicit("Not working. dto.Customer retrieved from cache as uninitialized proxy")] + [Test] + public void ProjectedEntitiesAreCachable() + { + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + var dto = session.Query() + .WithOptions(o => o.SetCacheable(true)) + .Where(x => x.OrderId == 10248) + .Select(x => new { x.Customer, Order = x }) + .FirstOrDefault(); + + Assert.That(dto, Is.Not.Null, "dto should not be null"); + Assert.That(dto.Order, Is.Not.Null, "dto.Order should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Order), Is.True, "dto.Order should be initialized"); + Assert.That(dto.Customer, Is.Not.Null, "dto.Customer should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Customer), Is.True, "dto.Customer from cache should be initialized"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + + Sfi.Statistics.Clear(); + session.Clear(); + + dto = session.Query() + .WithOptions(o => o.SetCacheable(true)) + .Where(x => x.OrderId == 10248) + .Select(x => new { x.Customer, Order = x }) + .FirstOrDefault(); + + Assert.That(dto, Is.Not.Null, "dto from cache should not be null"); + Assert.That(dto.Order, Is.Not.Null, "dto.Order from cache should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Order), Is.True, "dto.Order from cache should be initialized"); + Assert.That(dto.Customer, Is.Not.Null, "dto.Customer from cache should not be null"); + Assert.That(NHibernateUtil.IsInitialized(dto.Customer), Is.True, "dto.Customer from cache should be initialized"); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + [Test] + public void CacheHqlQueryWithFetchAndTransformerThatChangeTuple() + { + if (!TestDialect.SupportsDuplicatedColumnAliases) + Assert.Ignore("Ignored due to GH-2092"); + + Sfi.Statistics.Clear(); + Sfi.EvictQueries(); + + // the combination of query and transformer doesn't make sense. + // It's simply used as example of returned data being transformed before caching leading to mismatch between + // Loader.ResultTypes collection and provided tuple + var order = session.CreateQuery("select o.Employee.FirstName, o from Order o join fetch o.Customer where o.OrderId = :id") + .SetInt32("id", 10248) + .SetCacheable(true) + .SetResultTransformer(Transformers.RootEntity) + .UniqueResult(); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count"); + Assert.That(order, Is.Not.Null); + Assert.That(order.Customer, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True); + + session.Clear(); + Sfi.Statistics.Clear(); + + order = session.CreateQuery("select o.Employee.FirstName, o from Order o join fetch o.Customer where o.OrderId = :id") + .SetInt32("id", 10248) + .SetCacheable(true) + .SetResultTransformer(Transformers.RootEntity) + .UniqueResult(); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + Assert.That(order, Is.Not.Null); + Assert.That(order.Customer, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True); + } private static void AssertFetchedOrder(Order order) { + Assert.That(NHibernateUtil.IsInitialized(order), "Expected the order to be initialized"); + Assert.That(order.Customer, Is.Not.Null, "Expected the fetched Customer to be not null"); Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized"); Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items"); var orderLine = order.OrderLines.First(); + Assert.That(orderLine.Product, Is.Not.Null, "Expected the fetched Product to be not null"); Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized"); Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized"); } diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 33dff86b077..6367dbd9711 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -27,7 +27,6 @@ using NHibernate.Exceptions; using NHibernate.Hql.Util; using NHibernate.Impl; -using NHibernate.Intercept; using NHibernate.Param; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; diff --git a/src/NHibernate/Cache/QueryCacheResultBuilder.cs b/src/NHibernate/Cache/QueryCacheResultBuilder.cs index f3f145ddad3..fff6fe08d75 100644 --- a/src/NHibernate/Cache/QueryCacheResultBuilder.cs +++ b/src/NHibernate/Cache/QueryCacheResultBuilder.cs @@ -1,12 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using NHibernate.Collection; -using NHibernate.Engine; -using NHibernate.Persister.Collection; using NHibernate.Type; namespace NHibernate.Cache @@ -17,56 +12,34 @@ namespace NHibernate.Cache public sealed class QueryCacheResultBuilder { private readonly IType[] _resultTypes; - private readonly IType[] _cacheTypes; - private readonly List _entityFetchIndexes = new List(); - private readonly List _collectionFetchIndexes = new List(); - private readonly bool _hasFetches; + private readonly Loader.Loader.QueryCacheInfo _cacheInfo; + public static bool IsCacheWithFetches(Loader.Loader loader) + { + return loader.CacheTypes.Length > loader.ResultTypes.Length; + } + internal QueryCacheResultBuilder(Loader.Loader loader) { _resultTypes = loader.ResultTypes; - _cacheTypes = loader.CacheTypes; - if (loader.EntityFetches != null) + if (IsCacheWithFetches(loader)) { - for (var i = 0; i < loader.EntityFetches.Length; i++) - { - if (loader.EntityFetches[i]) - { - _entityFetchIndexes.Add(i); - } - } - - _hasFetches = _entityFetchIndexes.Count > 0; - } - - if (loader.CollectionFetches == null) - { - return; + _cacheInfo = loader.CacheInfo; } - - for (var i = 0; i < loader.CollectionFetches.Length; i++) - { - if (loader.CollectionFetches[i]) - { - _collectionFetchIndexes.Add(i); - } - } - - _hasFetches = _hasFetches || _collectionFetchIndexes.Count > 0; } internal IList Result { get; } = new List(); internal void AddRow(object result, object[] entities, IPersistentCollection[] collections) { - if (!_hasFetches) + if (_cacheInfo == null) { Result.Add(result); return; } - var row = new object[_cacheTypes.Length]; + var row = new object[_cacheInfo.CacheTypes.Length]; if (_resultTypes.Length == 1) { row[0] = result; @@ -77,14 +50,17 @@ internal void AddRow(object result, object[] entities, IPersistentCollection[] c } var i = _resultTypes.Length; - foreach (var index in _entityFetchIndexes) + foreach (var index in _cacheInfo.AdditionalEntities) { row[i++] = entities[index]; } - foreach (var index in _collectionFetchIndexes) + if (collections != null) { - row[i++] = collections[index]; + foreach (var collection in collections) + { + row[i++] = collection; + } } Result.Add(row); @@ -92,7 +68,7 @@ internal void AddRow(object result, object[] entities, IPersistentCollection[] c internal IList GetResultList(IList cacheList) { - if (!_hasFetches) + if (_cacheInfo == null) { return cacheList; } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs index 188fc67f0db..a3e4c9af4e1 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs @@ -56,7 +56,6 @@ public void InitializeDerivedSelectClause(FromClause fromClause) ASTAppender appender = new ASTAppender(ASTFactory, this); // Get ready to start adding nodes. int size = fromElements.Count; - List sqlResultTypeList = new List(size); List queryReturnTypeList = new List(size); int k = 0; @@ -78,7 +77,6 @@ public void InitializeDerivedSelectClause(FromClause fromClause) } _fromElementsForLoad.Add(fromElement); - sqlResultTypeList.Add(type); // Generate the select expression. string text = fromElement.RenderIdentifierSelect(size, k); diff --git a/src/NHibernate/Loader/Criteria/CriteriaLoader.cs b/src/NHibernate/Loader/Criteria/CriteriaLoader.cs index e3b816d2661..e3a5ff5dfda 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaLoader.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaLoader.cs @@ -75,7 +75,7 @@ public CriteriaLoader(IOuterJoinLoadable persister, ISessionFactoryImplementor f userAliases = walker.UserAliases; ResultTypes = walker.ResultTypes; includeInResultRow = walker.IncludeInResultRow; - resultRowLength = ArrayHelper.CountTrue(IncludeInResultRow); + resultRowLength = ArrayHelper.CountTrue(includeInResultRow); childFetchEntities = walker.ChildFetchEntities; EntityFetchLazyProperties = walker.EntityFetchLazyProperties; // fill caching objects only if there is a projection @@ -85,6 +85,10 @@ public CriteriaLoader(IOuterJoinLoadable persister, ISessionFactoryImplementor f } PostInstantiate(); + if (!translator.HasProjection) + { + CachePersistersWithCollections(ArrayHelper.IndexesOf(includeInResultRow, true)); + } } // Not ported: scroll (not supported) diff --git a/src/NHibernate/Loader/Custom/CustomLoader.cs b/src/NHibernate/Loader/Custom/CustomLoader.cs index 504ce824a19..dea693cf540 100644 --- a/src/NHibernate/Loader/Custom/CustomLoader.cs +++ b/src/NHibernate/Loader/Custom/CustomLoader.cs @@ -275,7 +275,7 @@ public override ILoadable[] EntityPersisters get { return entityPersisters; } } - protected override ICollectionPersister[] CollectionPersisters + protected internal override ICollectionPersister[] CollectionPersisters { get { return collectionPersisters; } } diff --git a/src/NHibernate/Loader/Hql/QueryLoader.cs b/src/NHibernate/Loader/Hql/QueryLoader.cs index 95ce94e6693..9b26eab0937 100644 --- a/src/NHibernate/Loader/Hql/QueryLoader.cs +++ b/src/NHibernate/Loader/Hql/QueryLoader.cs @@ -44,7 +44,6 @@ public partial class QueryLoader : BasicLoader private readonly NullableDictionary _sqlAliasByEntityAlias = new NullableDictionary(); private int _selectLength; private LockMode[] _defaultLockModes; - private IType[] _cacheTypes; private ISet _uncacheableCollectionPersisters; private Dictionary[] _collectionUserProvidedAliases; private IReadOnlyDictionary _entityByResultTypeDic; @@ -200,13 +199,11 @@ protected override string[] CollectionSuffixes get { return _collectionSuffixes; } } - protected override ICollectionPersister[] CollectionPersisters + protected internal override ICollectionPersister[] CollectionPersisters { get { return _collectionPersisters; } } - public override IType[] CacheTypes => _cacheTypes; - protected override IDictionary GetCollectionUserProvidedAlias(int index) { return _collectionUserProvidedAliases?[index]; @@ -232,7 +229,6 @@ private void Initialize(SelectClause selectClause) _collectionPersisters = new IQueryableCollection[length]; _collectionOwners = new int[length]; _collectionSuffixes = new string[length]; - CollectionFetches = new bool[length]; if (collectionFromElements.Any(qc => qc.QueryableCollection.IsManyToMany)) _collectionUserProvidedAliases = new Dictionary[length]; @@ -244,7 +240,6 @@ private void Initialize(SelectClause selectClause) // collectionSuffixes[i] = collectionFromElement.getColumnAliasSuffix(); // collectionSuffixes[i] = Integer.toString( i ) + "_"; _collectionSuffixes[i] = collectionFromElement.CollectionSuffix; - CollectionFetches[i] = collectionFromElement.IsFetch; } } @@ -258,8 +253,6 @@ private void Initialize(SelectClause selectClause) _includeInSelect = new bool[size]; _owners = new int[size]; _ownerAssociationTypes = new EntityType[size]; - EntityFetches = new bool[size]; - var cacheTypes = new List(ResultTypes); for (int i = 0; i < size; i++) { @@ -282,11 +275,6 @@ private void Initialize(SelectClause selectClause) _sqlAliasSuffixes[i] = (size == 1) ? "" : i + "_"; // sqlAliasSuffixes[i] = element.getColumnAliasSuffix(); _includeInSelect[i] = !element.IsFetch; - EntityFetches[i] = element.IsFetch; - if (element.IsFetch) - { - cacheTypes.Add(_entityPersisters[i].Type); - } if (_includeInSelect[i]) { _selectLength++; @@ -325,16 +313,10 @@ private void Initialize(SelectClause selectClause) } } - if (_collectionPersisters != null) - { - cacheTypes.AddRange(_collectionPersisters.Where((t, i) => CollectionFetches[i]).Select(t => t.CollectionType)); - } - - _cacheTypes = cacheTypes.ToArray(); - //NONE, because its the requested lock mode, not the actual! _defaultLockModes = ArrayHelper.Fill(LockMode.None, size); _uncacheableCollectionPersisters = _queryTranslator.UncacheableCollectionPersisters; + CachePersistersWithCollections(ArrayHelper.IndexesOf(_includeInSelect, true)); } public IList List(ISessionImplementor session, QueryParameters queryParameters) diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 988d08cef98..49fd0974e38 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -17,7 +17,6 @@ using NHibernate.Exceptions; using NHibernate.Hql.Util; using NHibernate.Impl; -using NHibernate.Intercept; using NHibernate.Param; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; @@ -52,8 +51,21 @@ namespace NHibernate.Loader /// public abstract partial class Loader { - private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(Loader)); + /// + /// DTO for providing all query cache related details + /// + public sealed class QueryCacheInfo + { + public IType[] CacheTypes { get; set; } + + /// + /// Loader.EntityPersister indexes to be cached. + /// + public IReadOnlyList AdditionalEntities { get; set; } + } + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(Loader)); + private Lazy _cacheInfo; private readonly ISessionFactoryImplementor _factory; private readonly SessionFactoryHelper _helper; private ColumnNameCache _columnNameCache; @@ -156,11 +168,18 @@ public virtual bool IsSubselectLoadingEnabled /// public IType[] ResultTypes { get; protected set; } - public bool[] EntityFetches { get; protected set; } + public IType[] CacheTypes => CacheInfo?.CacheTypes ?? ResultTypes; - public bool[] CollectionFetches { get; protected set; } + public virtual QueryCacheInfo CacheInfo => _cacheInfo?.Value; - public virtual IType[] CacheTypes => ResultTypes; + /// + /// Cache all additional persisters and collection persisters that were loaded by query (fetched entities and collections) + /// + /// Persister indexes that are cached as part of query result (so present in ResultTypes) + protected void CachePersistersWithCollections(IEnumerable resultTypePersisters) + { + _cacheInfo = new Lazy(() => GetQueryCacheInfo(resultTypePersisters)); + } public ISessionFactoryImplementor Factory { @@ -186,7 +205,7 @@ public ISessionFactoryImplementor Factory /// An (optional) persister for a collection to be initialized; only collection loaders /// return a non-null value /// - protected virtual ICollectionPersister[] CollectionPersisters + protected internal virtual ICollectionPersister[] CollectionPersisters { get { return null; } } @@ -1867,9 +1886,11 @@ internal QueryKey GenerateQueryKey(ISessionImplementor session, QueryParameters private CacheableResultTransformer CreateCacheableResultTransformer(QueryParameters queryParameters) { + bool skipTransformer = QueryCacheResultBuilder.IsCacheWithFetches(this); + return CacheableResultTransformer.Create( queryParameters.ResultTransformer, ResultRowAliases, IncludeInResultRow, - queryParameters.HasAutoDiscoverScalarTypes, SqlString); + queryParameters.HasAutoDiscoverScalarTypes, SqlString, skipTransformer); } private IList GetResultFromQueryCache( @@ -2080,6 +2101,31 @@ protected bool TryGetLimitString(Dialect.Dialect dialect, SqlString queryString, return false; } + private QueryCacheInfo GetQueryCacheInfo(IEnumerable resultTypePersisters) + { + var resultTypes = ResultTypes.EmptyIfNull(); + + var cacheTypes = new List(resultTypes.Count + EntityPersisters.Length + CollectionPersisters?.Length ?? 0); + cacheTypes.AddRange(resultTypes); + + int[] additionalEntities = null; + if (EntityPersisters.Length > 0) + { + additionalEntities = Enumerable.Range(0, EntityPersisters.Length).Except(resultTypePersisters).ToArray(); + cacheTypes.AddRange(additionalEntities.Select(i => EntityPersisters[i].EntityMetamodel.EntityType)); + } + + cacheTypes.AddRange(CollectionPersisters.EmptyIfNull().Select(p => p.CollectionType)); + + return cacheTypes.Count == resultTypes.Count + ? null + : new QueryCacheInfo + { + CacheTypes = cacheTypes.ToArray(), + AdditionalEntities = additionalEntities.EmptyIfNull(), + }; + } + #endregion } } diff --git a/src/NHibernate/Loader/OuterJoinLoader.cs b/src/NHibernate/Loader/OuterJoinLoader.cs index eb5d515b114..04874e5ef5e 100644 --- a/src/NHibernate/Loader/OuterJoinLoader.cs +++ b/src/NHibernate/Loader/OuterJoinLoader.cs @@ -94,7 +94,7 @@ protected override string[] Aliases get { return aliases; } } - protected override ICollectionPersister[] CollectionPersisters + protected internal override ICollectionPersister[] CollectionPersisters { get { return collectionPersisters; } } diff --git a/src/NHibernate/Transform/CacheableResultTransformer.cs b/src/NHibernate/Transform/CacheableResultTransformer.cs index 97d64f459ff..caed8a31221 100644 --- a/src/NHibernate/Transform/CacheableResultTransformer.cs +++ b/src/NHibernate/Transform/CacheableResultTransformer.cs @@ -24,6 +24,7 @@ public class CacheableResultTransformer : IResultTransformer public bool AutoDiscoverTypes { get; } private readonly SqlString _autoDiscoveredQuery; + private readonly bool _skipTransformer; private int _tupleLength; private int _tupleSubsetLength; @@ -60,7 +61,7 @@ public class CacheableResultTransformer : IResultTransformer /// a CacheableResultTransformer that is used to transform /// tuples to a value(s) that can be cached. // Since v5.1 - [Obsolete("Please use overload with autoDiscoverTypes parameter.")] + [Obsolete("Please use overload with skipTransformer parameter.")] public static CacheableResultTransformer Create(IResultTransformer transformer, string[] aliases, bool[] includeInTuple) @@ -68,6 +69,17 @@ public static CacheableResultTransformer Create(IResultTransformer transformer, return Create(transformer, aliases, includeInTuple, false, null); } + // Since 5.2 + [Obsolete("Please use overload with skipTransformer parameter.")] + public static CacheableResultTransformer Create( + IResultTransformer transformer, string[] aliases, bool[] includeInTuple, bool autoDiscoverTypes, + SqlString autoDiscoveredQuery) + { + return autoDiscoverTypes + ? Create(autoDiscoveredQuery) + : Create(includeInTuple, GetIncludeInTransform(transformer, aliases, includeInTuple), false); + } + /// /// Returns a CacheableResultTransformer that is used to transform /// tuples to a value(s) that can be cached. @@ -84,15 +96,25 @@ public static CacheableResultTransformer Create(IResultTransformer transformer, /// Indicates if types auto-discovery is enabled. /// If , the query for which they /// will be autodiscovered. + /// If true cache results untransformed. /// a CacheableResultTransformer that is used to transform /// tuples to a value(s) that can be cached. public static CacheableResultTransformer Create( - IResultTransformer transformer, string[] aliases, bool[] includeInTuple, bool autoDiscoverTypes, - SqlString autoDiscoveredQuery) + IResultTransformer transformer, + string[] aliases, + bool[] includeInTuple, + bool autoDiscoverTypes, + SqlString autoDiscoveredQuery, + bool skipTransformer) { return autoDiscoverTypes ? Create(autoDiscoveredQuery) - : Create(includeInTuple, GetIncludeInTransform(transformer, aliases, includeInTuple)); + : Create( + includeInTuple, + skipTransformer + ? null + : GetIncludeInTransform(transformer, aliases, includeInTuple), + skipTransformer); } /// @@ -106,11 +128,12 @@ public static CacheableResultTransformer Create( /// must be non-null /// Indexes that are included in the transformation. /// null if all elements in the tuple are included. + /// /// a CacheableResultTransformer that is used to transform /// tuples to a value(s) that can be cached. - private static CacheableResultTransformer Create(bool[] includeInTuple, bool[] includeInTransform) + private static CacheableResultTransformer Create(bool[] includeInTuple, bool[] includeInTransform, bool skipTransformer) { - return new CacheableResultTransformer(includeInTuple, includeInTransform); + return new CacheableResultTransformer(includeInTuple, includeInTransform, skipTransformer); } private static CacheableResultTransformer Create(SqlString autoDiscoveredQuery) @@ -136,8 +159,9 @@ private static bool[] GetIncludeInTransform(IResultTransformer transformer, stri return resultTransformer.IncludeInTransform(aliases, tupleLength); } - private CacheableResultTransformer(bool[] includeInTuple, bool[] includeInTransform) + private CacheableResultTransformer(bool[] includeInTuple, bool[] includeInTransform, bool skipTransformer) { + _skipTransformer = skipTransformer; InitializeTransformer(includeInTuple, includeInTransform); } @@ -212,7 +236,7 @@ public IList RetransformResults(IList transformedResults, if (_includeInTuple == null) throw new InvalidOperationException("This transformer is not initialized"); - if (!HasSameParameters(Create(transformer, aliases, includeInTuple, false, null))) + if (!HasSameParameters(Create(transformer, aliases, includeInTuple, false, null, _skipTransformer))) { throw new InvalidOperationException( "this CacheableResultTransformer is inconsistent with specified arguments; cannot re-transform" @@ -220,7 +244,11 @@ public IList RetransformResults(IList transformedResults, } bool requiresRetransform = true; string[] aliasesToUse = aliases == null ? null : Index(aliases); - if (transformer.Equals(_actualTransformer)) + + if (_skipTransformer) + { + } + else if (transformer.Equals(_actualTransformer)) { requiresRetransform = false; } diff --git a/src/NHibernate/Util/ArrayHelper.cs b/src/NHibernate/Util/ArrayHelper.cs index 52d24942bc9..f078cfa8607 100644 --- a/src/NHibernate/Util/ArrayHelper.cs +++ b/src/NHibernate/Util/ArrayHelper.cs @@ -180,6 +180,15 @@ public static int CountTrue(bool[] array) return array.Count(t => t); } + internal static IEnumerable IndexesOf(T[] array, T value) + { + for (int i = 0; i < array.Length; i++) + { + if (EqualityComparer.Default.Equals(array[i], value)) + yield return i; + } + } + public static bool ArrayEquals(T[] a, T[] b) { return ArrayComparer.Default.Equals(a, b); diff --git a/src/NHibernate/Util/EnumerableExtensions.cs b/src/NHibernate/Util/EnumerableExtensions.cs index b0e35892b06..0fb37b8ffe3 100644 --- a/src/NHibernate/Util/EnumerableExtensions.cs +++ b/src/NHibernate/Util/EnumerableExtensions.cs @@ -102,5 +102,10 @@ internal static IList ToIList(this IEnumerable list) { return list as IList ?? list.ToList(); } + + internal static IReadOnlyList EmptyIfNull(this IReadOnlyList list) + { + return list ?? Array.Empty(); + } } }