diff --git a/src/NHibernate.Test/Async/Futures/LinqFutureFixture.cs b/src/NHibernate.Test/Async/Futures/LinqFutureFixture.cs index 0e4ff1fe069..4faf15640de 100644 --- a/src/NHibernate.Test/Async/Futures/LinqFutureFixture.cs +++ b/src/NHibernate.Test/Async/Futures/LinqFutureFixture.cs @@ -176,6 +176,36 @@ public async Task CanUseFutureQueryAsync() } } + [Test] + public async Task CanUseFutureQueryAndQueryOverForSatelessSessionAsync() + { + IgnoreThisTestIfMultipleQueriesArentSupportedByDriver(); + + using (var s = Sfi.OpenStatelessSession()) + { + var persons10 = s.Query() + .Take(10) + .ToFuture(); + var persons5 = s.QueryOver() + .Take(5) + .Future(); + + using (var logSpy = new SqlLogSpy()) + { + foreach (var person in await (persons5.GetEnumerableAsync())) + { + } + + foreach (var person in await (persons10.GetEnumerableAsync())) + { + } + + var events = logSpy.Appender.GetEvents(); + Assert.AreEqual(1, events.Length); + } + } + } + [Test] public async Task CanUseFutureQueryWithAnonymousTypeAsync() { @@ -412,5 +442,120 @@ public async Task UsingManyParametersAndQueries_DoesNotCauseParameterNameCollisi await (tx.CommitAsync()); } } + + [Test] + public async Task FutureCombineCachedAndNonCachedQueriesAsync() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var p1 = new Person + { + Name = "Person name", + Age = 15 + }; + var p2 = new Person + { + Name = "Person name", + Age = 20 + }; + + await (s.SaveAsync(p1)); + await (s.SaveAsync(p2)); + await (tx.CommitAsync()); + } + + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + for (var i = 0; i < 5; i++) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + list.Add(query.WithOptions(x => x.SetCacheable(true)).ToFuture()); + } + + foreach (var query in list) + { + var result = (await (query.GetEnumerableAsync())).ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + } + + //Check query.List returns data from cache + Sfi.Statistics.IsStatisticsEnabled = true; + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + for (var i = 0; i < 5; i++) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + + list.Add(await (query.WithOptions(x => x.SetCacheable(true)).ToListAsync())); + } + + foreach (var query in list) + { + var result = query.ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Queries must be retrieved from cache"); + } + + //Check another Future returns data from cache + Sfi.Statistics.Clear(); + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + //Reverse order of queries added to cache + for (var i = 5 - 1; i >= 0; i--) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + + list.Add(query.WithOptions(x => x.SetCacheable(true)).ToFuture()); + } + + foreach (var query in list) + { + var result = (await (query.GetEnumerableAsync())).ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + + Assert.That(Sfi.Statistics.PrepareStatementCount , Is.EqualTo(0), "Future queries must be retrieved from cache"); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + await (s.DeleteAsync("from Person")); + await (tx.CommitAsync()); + } + } + + [Test] + public async Task FutureAutoFlushAsync() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Auto; + var p1 = new Person + { + Name = "Person name", + Age = 15 + }; + await (s.SaveAsync(p1)); + await (s.FlushAsync()); + + await (s.DeleteAsync(p1)); + var count = await (s.QueryOver().ToRowCountQuery().FutureValue().GetValueAsync()); + await (tx.CommitAsync()); + + Assert.That(count, Is.EqualTo(0), "Session wasn't auto flushed."); + } + } } } diff --git a/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs b/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs new file mode 100644 index 00000000000..c7ece785054 --- /dev/null +++ b/src/NHibernate.Test/Async/Futures/QueryBatchFixture.cs @@ -0,0 +1,453 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NHibernate.Multi; +using NHibernate.Transform; +using NUnit.Framework; + +namespace NHibernate.Test.Futures +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class QueryBatchFixtureAsync : TestCaseMappingByCode + { + private Guid _parentId; + private Guid _eagerId; + + [Test] + public async Task CanCombineCriteriaAndHqlInFutureAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var future1 = session.QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt()).Future(); + + var future2 = session.Query().Where(ec => ec.Version > 2).ToFuture(); + var future3 = session.Query().Select(sc => sc.Name).ToFuture(); + + var future4 = session + .Query() + .ToFutureValue(sc => sc.FirstOrDefault()); + + Assert.That((await (future1.GetEnumerableAsync())).Count(), Is.GreaterThan(0), "Empty results are not expected"); + Assert.That((await (future2.GetEnumerableAsync())).Count(), Is.EqualTo(0), "This query should not return results"); + Assert.That((await (future3.GetEnumerableAsync())).Count(), Is.GreaterThan(1), "Empty results are not expected"); + Assert.That(await (future4.GetValueAsync()), Is.Not.Null, "Loaded entity should not be null"); + + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + + [Test] + public async Task CanCombineCriteriaAndHqlInBatchAsync() + { + using (var session = OpenSession()) + { + var batch = session + .CreateQueryBatch() + + .Add( + session + .QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt())) + + .Add("queryOver", session.QueryOver().Where(x => x.Version >= 1)) + + .Add(session.Query().Where(ec => ec.Version > 2)) + + .Add("sql", + session.CreateSQLQuery( + $"select * from {nameof(EntitySimpleChild)}") + .AddEntity(typeof(EntitySimpleChild))); + + using (var sqlLog = new SqlLogSpy()) + { + await (batch.GetResultAsync(0, CancellationToken.None)); + await (batch.GetResultAsync("queryOver", CancellationToken.None)); + await (batch.GetResultAsync(2, CancellationToken.None)); + await (batch.GetResultAsync("sql", CancellationToken.None)); + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + } + + [Test] + public async Task CanCombineCriteriaAndHqlInBatchAsFutureAsync() + { + using (var session = OpenSession()) + { + var batch = session + .CreateQueryBatch(); + + var future1 = batch.AddAsFuture( + session + .QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt())); + + var future2 = batch.AddAsFutureValue(session.QueryOver().Where(x => x.Version >= 1).Select(x => x.Id)); + + var future3 = batch.AddAsFuture(session.Query().Where(ec => ec.Version > 2)); + var future4 = batch.AddAsFutureValue(session.Query().Where(ec => ec.Version > 2), ec => ec.FirstOrDefault()); + + var future5 = batch.AddAsFuture( + session.CreateSQLQuery( + $"select * from {nameof(EntitySimpleChild)}") + .AddEntity(typeof(EntitySimpleChild))); + + using (var sqlLog = new SqlLogSpy()) + { + var future1List = (await (future1.GetEnumerableAsync())).ToList(); + var future2Value = await (future2.GetValueAsync()); + var future3List = (await (future3.GetEnumerableAsync())).ToList(); + var future4Value = await (future4.GetValueAsync()); + var future5List = (await (future5.GetEnumerableAsync())).ToList(); + + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + } + + [Test] + public async Task CanFetchCollectionInBatchAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + + var q1 = session.QueryOver() + .Where(x => x.Version >= 0); + + batch.Add(q1); + batch.Add(session.Query().Fetch(c => c.ChildrenList)); + await (batch.ExecuteAsync(CancellationToken.None)); + + var parent = await (session.LoadAsync(_parentId)); + Assert.That(NHibernateUtil.IsInitialized(parent), Is.True); + Assert.That(NHibernateUtil.IsInitialized(parent.ChildrenList), Is.True); + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + + [Test] + public async Task AfterLoadCallbackAsync() + { + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + IList results = null; + int count = 0; + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), r => results = r); + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), ec => ec.Count(), r => count = r); + await (batch.ExecuteAsync(CancellationToken.None)); + + Assert.That(results, Is.Not.Null); + Assert.That(count, Is.GreaterThan(0)); + } + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + IList results = null; + int count = 0; + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), r => results = r); + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), ec => ec.Count(), r => count = r); + + await (batch.ExecuteAsync(CancellationToken.None)); + + Assert.That(results, Is.Not.Null); + Assert.That(count, Is.GreaterThan(0)); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(0), "Query is expected to be retrieved from cache"); + } + } + + //NH-3350 (Duplicate records using Future()) + [Test] + public async Task SameCollectionFetchesAsync() + { + using (var session = OpenSession()) + { + var entiyComplex = session.QueryOver().Where(c => c.Id == _parentId).FutureValue(); + + session.QueryOver() + .Fetch(SelectMode.Fetch, ec => ec.ChildrenList) + .Where(c => c.Id == _parentId).Future(); + + session.QueryOver() + .Fetch(SelectMode.Fetch, ec => ec.ChildrenList) + .Where(c => c.Id == _parentId).Future(); + + var parent = await (entiyComplex.GetValueAsync()); + Assert.That(NHibernateUtil.IsInitialized(parent), Is.True); + Assert.That(NHibernateUtil.IsInitialized(parent.ChildrenList), Is.True); + Assert.That(parent.ChildrenList.Count, Is.EqualTo(2)); + + } + } + + //NH-3864 - Cacheable Multicriteria/Future'd query with aliased join throw exception + [Test] + public void CacheableCriteriaWithAliasedJoinFutureAsync() + { + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + var ecFuture = session.QueryOver() + .JoinAlias(c => c.Child1, () => child1) + .Where(c => c.Id == _parentId) + .Cacheable() + .FutureValue(); + EntityComplex value = null; + Assert.DoesNotThrowAsync(async () => value = await (ecFuture.GetValueAsync())); + Assert.That(value, Is.Not.Null); + } + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + var ecFuture = session.QueryOver() + .JoinAlias(c => c.Child1, () => child1) + .Where(c => c.Id == _parentId) + .Cacheable() + .FutureValue(); + EntityComplex value = null; + Assert.DoesNotThrowAsync(async () => value = await (ecFuture.GetValueAsync())); + Assert.That(value, Is.Not.Null); + + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(0), "Query is expected to be retrieved from cache"); + } + } + + //NH-3334 - 'collection is not associated with any session' upon refreshing objects from QueryOver<>().Future<>() + [KnownBug("NH-3334")] + [Test] + public async Task RefreshFutureWithEagerCollectionsAsync() + { + using (var session = OpenSession()) + { + var ecFutureList = session.QueryOver().Future(); + + foreach(var ec in await (ecFutureList.GetEnumerableAsync())) + { + //trouble causes ec.ChildrenListEager with eager select mapping + Assert.DoesNotThrowAsync(() => session.RefreshAsync(ec), "session.Refresh should not throw exception"); + } + } + } + + //Related to NH-3334. Eager mappings are not fetched by Future + [KnownBug("NH-3334")] + [Test] + public async Task FutureForEagerMappedCollectionAsync() + { + //Note: This behavior might be considered as feature but it's not documented. + //Quirk: if this query is also cached - results will be still eager loaded when values retrieved from cache + using (var session = OpenSession()) + { + var futureValue = session.QueryOver().Where(e => e.Id == _eagerId).FutureValue(); + + Assert.That(await (futureValue.GetValueAsync()), Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(await (futureValue.GetValueAsync())), Is.True); + Assert.That(NHibernateUtil.IsInitialized((await (futureValue.GetValueAsync())).ChildrenListEager), Is.True); + Assert.That(NHibernateUtil.IsInitialized((await (futureValue.GetValueAsync())).ChildrenListSubselect), Is.True); + } + } + + #region Test Setup + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + + rc.Version(ep => ep.Version, vm => { }); + + rc.Property(x => x.Name); + + rc.Property(ep => ep.LazyProp, m => m.Lazy(true)); + + rc.ManyToOne(ep => ep.Child1, m => m.Column("Child1Id")); + rc.ManyToOne(ep => ep.Child2, m => m.Column("Child2Id")); + rc.ManyToOne(ep => ep.SameTypeChild, m => m.Column("SameTypeChildId")); + + rc.Bag( + ep => ep.ChildrenList, + m => + { + m.Cascade(Mapping.ByCode.Cascade.All); + m.Inverse(true); + }, + a => a.OneToMany()); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Parent); + rc.Property(x => x.Name); + }); + mapper.Class( + rc => + { + rc.Lazy(false); + + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + + rc.Bag(ep => ep.ChildrenListSubselect, + m => + { + m.Cascade(Mapping.ByCode.Cascade.All); + m.Inverse(true); + m.Fetch(CollectionFetchMode.Subselect); + m.Lazy(CollectionLazy.NoLazy); + }, + a => a.OneToMany()); + + rc.Bag(ep => ep.ChildrenListEager, + m => + { + m.Lazy(CollectionLazy.NoLazy); + }, + a => a.OneToMany()); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(c => c.Parent); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var child1 = new EntitySimpleChild + { + Name = "Child1", + }; + var child2 = new EntitySimpleChild + { + Name = "Child2" + }; + var complex = new EntityComplex + { + Name = "ComplexEnityParent", + Child1 = child1, + Child2 = child2, + LazyProp = "SomeBigValue", + SameTypeChild = new EntityComplex() + { + Name = "ComplexEntityChild" + }, + }; + child1.Parent = child2.Parent = complex; + + var eager = new EntityEager() + { + Name = "eager1", + }; + + var eager2 = new EntityEager() + { + Name = "eager2", + }; + eager.ChildrenListSubselect = new List() + { + new EntitySubselectChild() + { + Name = "subselect1", + Parent = eager, + }, + new EntitySubselectChild() + { + Name = "subselect2", + Parent = eager, + }, + }; + + session.Save(child1); + session.Save(child2); + session.Save(complex.SameTypeChild); + session.Save(complex); + session.Save(eager); + session.Save(eager2); + + session.Flush(); + transaction.Commit(); + + _parentId = complex.Id; + _eagerId = eager.Id; + } + } + + public class ListTransformerToInt : IResultTransformer + { + public object TransformTuple(object[] tuple, string[] aliases) + { + return tuple.Length == 1 ? tuple[0] : tuple; + } + + public IList TransformList(IList collection) + { + return new List() + { + 1, + 2, + 3, + 4, + }; + } + } + + private bool SupportsMultipleQueries => Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + #endregion Test Setup + } +} diff --git a/src/NHibernate.Test/Async/Legacy/MultiTableTest.cs b/src/NHibernate.Test/Async/Legacy/MultiTableTest.cs index 05ec6ba9012..5ca68ae0c14 100644 --- a/src/NHibernate.Test/Async/Legacy/MultiTableTest.cs +++ b/src/NHibernate.Test/Async/Legacy/MultiTableTest.cs @@ -15,6 +15,8 @@ using NHibernate.DomainModel; using NUnit.Framework; +using MultiEntity = NHibernate.DomainModel.Multi; + namespace NHibernate.Test.Legacy { using System.Threading.Tasks; @@ -207,7 +209,7 @@ public async Task MultiTableAsync() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -256,7 +258,7 @@ public async Task MultiTableAsync() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) await (s.LoadAsync(typeof(Multi), mid)); + multi = (MultiEntity) await (s.LoadAsync(typeof(MultiEntity), mid)); Assert.AreEqual("extra2", multi.ExtraProp); multi.ExtraProp = multi.ExtraProp + "3"; Assert.AreEqual("new name", multi.Name); @@ -269,9 +271,9 @@ public async Task MultiTableAsync() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) await (s.LoadAsync(typeof(Top), mid)); + multi = (MultiEntity) await (s.LoadAsync(typeof(Top), mid)); simp = (Top) await (s.LoadAsync(typeof(Top), sid)); - Assert.IsFalse(simp is Multi); + Assert.IsFalse(simp is MultiEntity); Assert.AreEqual("extra23", multi.ExtraProp); Assert.AreEqual("newer name", multi.Name); await (t.CommitAsync()); @@ -286,8 +288,8 @@ public async Task MultiTableAsync() while (enumer.MoveNext()) { object o = enumer.Current; - if ((o is Top) && !(o is Multi)) foundSimp = true; - if ((o is Multi) && !(o is SubMulti)) foundMulti = true; + if ((o is Top) && !(o is MultiEntity)) foundSimp = true; + if ((o is MultiEntity) && !(o is SubMulti)) foundMulti = true; if (o is SubMulti) foundSubMulti = true; } Assert.IsTrue(foundSimp); @@ -322,7 +324,7 @@ public async Task MultiTableAsync() s = OpenSession(); t = s.BeginTransaction(); if (TestDialect.SupportsSelectForUpdateOnOuterJoin) - multi = (Multi)await (s.LoadAsync(typeof(Top), mid, LockMode.Upgrade)); + multi = (MultiEntity)await (s.LoadAsync(typeof(Top), mid, LockMode.Upgrade)); simp = (Top) await (s.LoadAsync(typeof(Top), sid)); await (s.LockAsync(simp, LockMode.UpgradeNoWait)); await (t.CommitAsync()); @@ -342,7 +344,7 @@ public async Task MultiTableGeneratedIdAsync() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -370,7 +372,7 @@ public async Task MultiTableGeneratedIdAsync() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) await (s.LoadAsync(typeof(Multi), multiId)); + multi = (MultiEntity) await (s.LoadAsync(typeof(MultiEntity), multiId)); Assert.AreEqual("extra2", multi.ExtraProp); multi.ExtraProp += "3"; Assert.AreEqual("new name", multi.Name); @@ -383,11 +385,9 @@ public async Task MultiTableGeneratedIdAsync() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) await (s.LoadAsync(typeof(Top), multiId)); + multi = (MultiEntity) await (s.LoadAsync(typeof(Top), multiId)); simp = (Top) await (s.LoadAsync(typeof(Top), simpId)); - Assert.IsFalse(simp is Multi); - // Can't see the point of this test since the variable is declared as Multi! - //Assert.IsTrue( multi is Multi ); + Assert.IsFalse(simp is MultiEntity); Assert.AreEqual("extra23", multi.ExtraProp); Assert.AreEqual("newer name", multi.Name); await (t.CommitAsync()); @@ -402,8 +402,8 @@ public async Task MultiTableGeneratedIdAsync() foreach (object obj in enumer) { - if ((obj is Top) && !(obj is Multi)) foundSimp = true; - if ((obj is Multi) && !(obj is SubMulti)) foundMulti = true; + if ((obj is Top) && !(obj is MultiEntity)) foundSimp = true; + if ((obj is MultiEntity) && !(obj is SubMulti)) foundMulti = true; if (obj is SubMulti) foundSubMulti = true; } Assert.IsTrue(foundSimp); @@ -430,7 +430,7 @@ public async Task MultiTableGeneratedIdAsync() s = OpenSession(); t = s.BeginTransaction(); if (TestDialect.SupportsSelectForUpdateOnOuterJoin) - multi = (Multi) await (s.LoadAsync(typeof(Top), multiId, LockMode.Upgrade)); + multi = (MultiEntity) await (s.LoadAsync(typeof(Top), multiId, LockMode.Upgrade)); simp = (Top) await (s.LoadAsync(typeof(Top), simpId)); await (s.LockAsync(simp, LockMode.UpgradeNoWait)); await (t.CommitAsync()); @@ -456,7 +456,7 @@ public async Task MultiTableCollectionsAsync() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); Assert.AreEqual(0, (await (s.CreateQuery("from s in class Top").ListAsync())).Count); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -513,7 +513,7 @@ public async Task MultiTableCollectionsAsync() foreach (object obj in ls.Set) { if (obj is Top) foundSimple++; - if (obj is Multi) foundMulti++; + if (obj is MultiEntity) foundMulti++; } Assert.AreEqual(2, foundSimple); Assert.AreEqual(1, foundMulti); @@ -533,7 +533,7 @@ public async Task MultiTableManyToOneAsync() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); Assert.AreEqual(0, (await (s.CreateQuery("from s in class Top").ListAsync())).Count); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -578,7 +578,7 @@ public async Task MultiTableManyToOneAsync() Assert.AreSame(ls, ls.Other); Assert.AreSame(ls, ls.YetAnother); Assert.AreEqual("name", ls.Another.Name); - Assert.IsTrue(ls.Another is Multi); + Assert.IsTrue(ls.Another is MultiEntity); await (s.DeleteAsync(ls)); await (s.DeleteAsync(ls.Another)); await (t.CommitAsync()); @@ -590,7 +590,7 @@ public async Task MultiTableNativeIdAsync() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; object id = await (s.SaveAsync(multi)); Assert.IsNotNull(id); @@ -607,14 +607,14 @@ public async Task CollectionAsync() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi1 = new Multi(); + MultiEntity multi1 = new MultiEntity(); multi1.ExtraProp = "extra1"; - Multi multi2 = new Multi(); + MultiEntity multi2 = new MultiEntity(); multi2.ExtraProp = "extra2"; Po po = new Po(); multi1.Po = po; multi2.Po = po; - po.Set = new HashSet {multi1, multi2}; + po.Set = new HashSet {multi1, multi2}; po.List = new List {new SubMulti()}; object id = await (s.SaveAsync(po)); Assert.IsNotNull(id); diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH1989/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1989/Fixture.cs index 1b36b91e4d3..48f8bf99ba5 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH1989/Fixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1989/Fixture.cs @@ -163,7 +163,6 @@ public async Task SecondLevelCacheWithMixedCacheableAndNonCacheableFutureAsync() .SetCacheable(true) .FutureValue(); - // non cacheable Future causes batch to be non-cacheable int count = await (s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -190,7 +189,9 @@ public async Task SecondLevelCacheWithMixedCacheableAndNonCacheableFutureAsync() .FutureValue() .GetValueAsync()); - Assert.That(await (userFuture.GetValueAsync()), Is.Null, + Assert.That(await (userFuture.GetValueAsync()), Is.Not.Null, + "query results should come from cache"); + Assert.That(count, Is.EqualTo(0), "query results should not come from cache"); } } @@ -216,7 +217,6 @@ public async Task SecondLevelCacheWithMixedCacheRegionsFutureAsync() .SetCacheRegion("region1") .FutureValue(); - // different cache-region causes batch to be non-cacheable int count = await (s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -240,6 +240,13 @@ public async Task SecondLevelCacheWithMixedCacheRegionsFutureAsync() .SetCacheRegion("region1") .FutureValue(); + IFutureValue userFutureWrongRegion = + s.CreateCriteria() + .Add(Restrictions.NaturalId().Set("Name", "test")) + .SetCacheable(true) + .SetCacheRegion("region2") + .FutureValue(); + int count = await (s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -248,8 +255,23 @@ public async Task SecondLevelCacheWithMixedCacheRegionsFutureAsync() .FutureValue() .GetValueAsync()); - Assert.That(await (userFuture.GetValueAsync()), Is.Null, - "query results should not come from cache"); + int countWrongRegion = + await (s.CreateCriteria() + .SetProjection(Projections.RowCount()) + .SetCacheable(true) + .SetCacheRegion("region1") + .FutureValue() + .GetValueAsync()); + + Assert.That(await (userFuture.GetValueAsync()), Is.Not.Null, + "query results should come from cache"); + Assert.That(count, Is.EqualTo(1), + "query results should come from cache"); + + Assert.That(await (userFutureWrongRegion.GetValueAsync()), Is.Null, + "query results from wrong cache region"); + Assert.That(countWrongRegion, Is.EqualTo(0), + "query results from wrong cache region"); } } diff --git a/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs b/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs index 14ceae03078..e6920fa553c 100644 --- a/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs +++ b/src/NHibernate.Test/Async/QueryTest/MultiCriteriaFixture.cs @@ -474,5 +474,32 @@ public async Task UsingManyParametersAndQueries_DoesNotCauseParameterNameCollisi } } } + + //NH-2428 - Session.MultiCriteria and FlushMode.Auto inside transaction (GH865) + [Test] + public async Task MultiCriteriaAutoFlushAsync() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Auto; + var p1 = new Item + { + Name = "Person name", + Id = 15 + }; + await (s.SaveAsync(p1)); + await (s.FlushAsync()); + + await (s.DeleteAsync(p1)); + var multi = s.CreateMultiCriteria(); + multi.Add(s.QueryOver().ToRowCountQuery()); + var count = (int) ((IList) (await (multi.ListAsync()))[0])[0]; + await (tx.CommitAsync()); + + Assert.That(count, Is.EqualTo(0), "Session wasn't auto flushed."); + + } + } } } diff --git a/src/NHibernate.Test/ExpressionTest/SimpleExpressionFixture.cs b/src/NHibernate.Test/ExpressionTest/SimpleExpressionFixture.cs index afa05ed7e83..9968b348fa0 100644 --- a/src/NHibernate.Test/ExpressionTest/SimpleExpressionFixture.cs +++ b/src/NHibernate.Test/ExpressionTest/SimpleExpressionFixture.cs @@ -74,11 +74,11 @@ public void MisspelledPropertyWithNormalizedEntityPersister() { using (ISession session = factory.OpenSession()) { - CreateObjects(typeof(Multi), session); + CreateObjects(typeof(NHibernate.DomainModel.Multi), session); ICriterion expression = Expression.Eq("MisspelledProperty", DateTime.Now); Assert.Throws(() =>expression.ToSqlString(criteria, criteriaQuery)); } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/Futures/Entities.cs b/src/NHibernate.Test/Futures/Entities.cs new file mode 100644 index 00000000000..3c866f497fd --- /dev/null +++ b/src/NHibernate.Test/Futures/Entities.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.Futures +{ + public class EntityComplex + { + public virtual Guid Id { get; set; } + + public virtual int Version { get; set; } + + public virtual string Name { get; set; } + + public virtual string LazyProp { get; set; } + + public virtual EntitySimpleChild Child1 { get; set; } + public virtual EntitySimpleChild Child2 { get; set; } + public virtual EntityComplex SameTypeChild { get; set; } + + public virtual IList ChildrenList { get; set; } = new List(); + public virtual IList ChildrenListEmpty { get; set; } = new List(); + + } + + public class EntitySimpleChild + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual EntityComplex Parent { get; set; } + } + + public class EntitySubselectChild + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual EntityEager Parent { get; set; } + } + + public class EntityEager + { + public Guid Id { get; set; } + public string Name { get; set; } + + public IList ChildrenListSubselect { get; set; } + public IList ChildrenListEager { get; set; } //= new HashSet(); + } +} diff --git a/src/NHibernate.Test/Futures/LinqFutureFixture.cs b/src/NHibernate.Test/Futures/LinqFutureFixture.cs index 97e78ea2f6d..3a1015f6442 100644 --- a/src/NHibernate.Test/Futures/LinqFutureFixture.cs +++ b/src/NHibernate.Test/Futures/LinqFutureFixture.cs @@ -165,6 +165,36 @@ public void CanUseFutureQuery() } } + [Test] + public void CanUseFutureQueryAndQueryOverForSatelessSession() + { + IgnoreThisTestIfMultipleQueriesArentSupportedByDriver(); + + using (var s = Sfi.OpenStatelessSession()) + { + var persons10 = s.Query() + .Take(10) + .ToFuture(); + var persons5 = s.QueryOver() + .Take(5) + .Future(); + + using (var logSpy = new SqlLogSpy()) + { + foreach (var person in persons5.GetEnumerable()) + { + } + + foreach (var person in persons10.GetEnumerable()) + { + } + + var events = logSpy.Appender.GetEvents(); + Assert.AreEqual(1, events.Length); + } + } + } + [Test] public void CanUseFutureQueryWithAnonymousType() { @@ -401,5 +431,120 @@ public void UsingManyParametersAndQueries_DoesNotCauseParameterNameCollisions() tx.Commit(); } } + + [Test] + public void FutureCombineCachedAndNonCachedQueries() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var p1 = new Person + { + Name = "Person name", + Age = 15 + }; + var p2 = new Person + { + Name = "Person name", + Age = 20 + }; + + s.Save(p1); + s.Save(p2); + tx.Commit(); + } + + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + for (var i = 0; i < 5; i++) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + list.Add(query.WithOptions(x => x.SetCacheable(true)).ToFuture()); + } + + foreach (var query in list) + { + var result = query.GetEnumerable().ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + } + + //Check query.List returns data from cache + Sfi.Statistics.IsStatisticsEnabled = true; + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + for (var i = 0; i < 5; i++) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + + list.Add(query.WithOptions(x => x.SetCacheable(true)).ToList()); + } + + foreach (var query in list) + { + var result = query.ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Queries must be retrieved from cache"); + } + + //Check another Future returns data from cache + Sfi.Statistics.Clear(); + using (var s = Sfi.OpenSession()) + { + var list = new List>(); + //Reverse order of queries added to cache + for (var i = 5 - 1; i >= 0; i--) + { + var i1 = i; + var query = s.Query().Where(x => x.Age > i1); + + list.Add(query.WithOptions(x => x.SetCacheable(true)).ToFuture()); + } + + foreach (var query in list) + { + var result = query.GetEnumerable().ToList(); + Assert.That(result.Count, Is.EqualTo(2)); + } + + Assert.That(Sfi.Statistics.PrepareStatementCount , Is.EqualTo(0), "Future queries must be retrieved from cache"); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from Person"); + tx.Commit(); + } + } + + [Test] + public void FutureAutoFlush() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Auto; + var p1 = new Person + { + Name = "Person name", + Age = 15 + }; + s.Save(p1); + s.Flush(); + + s.Delete(p1); + var count = s.QueryOver().ToRowCountQuery().FutureValue().Value; + tx.Commit(); + + Assert.That(count, Is.EqualTo(0), "Session wasn't auto flushed."); + } + } } } diff --git a/src/NHibernate.Test/Futures/QueryBatchFixture.cs b/src/NHibernate.Test/Futures/QueryBatchFixture.cs new file mode 100644 index 00000000000..2e6989ec0d9 --- /dev/null +++ b/src/NHibernate.Test/Futures/QueryBatchFixture.cs @@ -0,0 +1,441 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NHibernate.Multi; +using NHibernate.Transform; +using NUnit.Framework; + +namespace NHibernate.Test.Futures +{ + [TestFixture] + public class QueryBatchFixture : TestCaseMappingByCode + { + private Guid _parentId; + private Guid _eagerId; + + [Test] + public void CanCombineCriteriaAndHqlInFuture() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var future1 = session.QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt()).Future(); + + var future2 = session.Query().Where(ec => ec.Version > 2).ToFuture(); + var future3 = session.Query().Select(sc => sc.Name).ToFuture(); + + var future4 = session + .Query() + .ToFutureValue(sc => sc.FirstOrDefault()); + + Assert.That(future1.GetEnumerable().Count(), Is.GreaterThan(0), "Empty results are not expected"); + Assert.That(future2.GetEnumerable().Count(), Is.EqualTo(0), "This query should not return results"); + Assert.That(future3.GetEnumerable().Count(), Is.GreaterThan(1), "Empty results are not expected"); + Assert.That(future4.Value, Is.Not.Null, "Loaded entity should not be null"); + + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + + [Test] + public void CanCombineCriteriaAndHqlInBatch() + { + using (var session = OpenSession()) + { + var batch = session + .CreateQueryBatch() + + .Add( + session + .QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt())) + + .Add("queryOver", session.QueryOver().Where(x => x.Version >= 1)) + + .Add(session.Query().Where(ec => ec.Version > 2)) + + .Add("sql", + session.CreateSQLQuery( + $"select * from {nameof(EntitySimpleChild)}") + .AddEntity(typeof(EntitySimpleChild))); + + using (var sqlLog = new SqlLogSpy()) + { + batch.GetResult(0); + batch.GetResult("queryOver"); + batch.GetResult(2); + batch.GetResult("sql"); + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + } + + [Test] + public void CanCombineCriteriaAndHqlInBatchAsFuture() + { + using (var session = OpenSession()) + { + var batch = session + .CreateQueryBatch(); + + var future1 = batch.AddAsFuture( + session + .QueryOver() + .Where(x => x.Version >= 0) + .TransformUsing(new ListTransformerToInt())); + + var future2 = batch.AddAsFutureValue(session.QueryOver().Where(x => x.Version >= 1).Select(x => x.Id)); + + var future3 = batch.AddAsFuture(session.Query().Where(ec => ec.Version > 2)); + var future4 = batch.AddAsFutureValue(session.Query().Where(ec => ec.Version > 2), ec => ec.FirstOrDefault()); + + var future5 = batch.AddAsFuture( + session.CreateSQLQuery( + $"select * from {nameof(EntitySimpleChild)}") + .AddEntity(typeof(EntitySimpleChild))); + + using (var sqlLog = new SqlLogSpy()) + { + var future1List = future1.GetEnumerable().ToList(); + var future2Value = future2.Value; + var future3List = future3.GetEnumerable().ToList(); + var future4Value = future4.Value; + var future5List = future5.GetEnumerable().ToList(); + + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + } + + [Test] + public void CanFetchCollectionInBatch() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + + var q1 = session.QueryOver() + .Where(x => x.Version >= 0); + + batch.Add(q1); + batch.Add(session.Query().Fetch(c => c.ChildrenList)); + batch.Execute(); + + var parent = session.Load(_parentId); + Assert.That(NHibernateUtil.IsInitialized(parent), Is.True); + Assert.That(NHibernateUtil.IsInitialized(parent.ChildrenList), Is.True); + if (SupportsMultipleQueries) + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1)); + } + } + + [Test] + public void AfterLoadCallback() + { + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + IList results = null; + int count = 0; + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), r => results = r); + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), ec => ec.Count(), r => count = r); + batch.Execute(); + + Assert.That(results, Is.Not.Null); + Assert.That(count, Is.GreaterThan(0)); + } + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var batch = session.CreateQueryBatch(); + IList results = null; + int count = 0; + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), r => results = r); + batch.Add(session.Query().WithOptions(o => o.SetCacheable(true)), ec => ec.Count(), r => count = r); + + batch.Execute(); + + Assert.That(results, Is.Not.Null); + Assert.That(count, Is.GreaterThan(0)); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(0), "Query is expected to be retrieved from cache"); + } + } + + //NH-3350 (Duplicate records using Future()) + [Test] + public void SameCollectionFetches() + { + using (var session = OpenSession()) + { + var entiyComplex = session.QueryOver().Where(c => c.Id == _parentId).FutureValue(); + + session.QueryOver() + .Fetch(SelectMode.Fetch, ec => ec.ChildrenList) + .Where(c => c.Id == _parentId).Future(); + + session.QueryOver() + .Fetch(SelectMode.Fetch, ec => ec.ChildrenList) + .Where(c => c.Id == _parentId).Future(); + + var parent = entiyComplex.Value; + Assert.That(NHibernateUtil.IsInitialized(parent), Is.True); + Assert.That(NHibernateUtil.IsInitialized(parent.ChildrenList), Is.True); + Assert.That(parent.ChildrenList.Count, Is.EqualTo(2)); + + } + } + + //NH-3864 - Cacheable Multicriteria/Future'd query with aliased join throw exception + [Test] + public void CacheableCriteriaWithAliasedJoinFuture() + { + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + var ecFuture = session.QueryOver() + .JoinAlias(c => c.Child1, () => child1) + .Where(c => c.Id == _parentId) + .Cacheable() + .FutureValue(); + EntityComplex value = null; + Assert.DoesNotThrow(() => value = ecFuture.Value); + Assert.That(value, Is.Not.Null); + } + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + var ecFuture = session.QueryOver() + .JoinAlias(c => c.Child1, () => child1) + .Where(c => c.Id == _parentId) + .Cacheable() + .FutureValue(); + EntityComplex value = null; + Assert.DoesNotThrow(() => value = ecFuture.Value); + Assert.That(value, Is.Not.Null); + + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(0), "Query is expected to be retrieved from cache"); + } + } + + //NH-3334 - 'collection is not associated with any session' upon refreshing objects from QueryOver<>().Future<>() + [KnownBug("NH-3334")] + [Test] + public void RefreshFutureWithEagerCollections() + { + using (var session = OpenSession()) + { + var ecFutureList = session.QueryOver().Future(); + + foreach(var ec in ecFutureList.GetEnumerable()) + { + //trouble causes ec.ChildrenListEager with eager select mapping + Assert.DoesNotThrow(() => session.Refresh(ec), "session.Refresh should not throw exception"); + } + } + } + + //Related to NH-3334. Eager mappings are not fetched by Future + [KnownBug("NH-3334")] + [Test] + public void FutureForEagerMappedCollection() + { + //Note: This behavior might be considered as feature but it's not documented. + //Quirk: if this query is also cached - results will be still eager loaded when values retrieved from cache + using (var session = OpenSession()) + { + var futureValue = session.QueryOver().Where(e => e.Id == _eagerId).FutureValue(); + + Assert.That(futureValue.Value, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(futureValue.Value), Is.True); + Assert.That(NHibernateUtil.IsInitialized(futureValue.Value.ChildrenListEager), Is.True); + Assert.That(NHibernateUtil.IsInitialized(futureValue.Value.ChildrenListSubselect), Is.True); + } + } + + #region Test Setup + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + + rc.Version(ep => ep.Version, vm => { }); + + rc.Property(x => x.Name); + + rc.Property(ep => ep.LazyProp, m => m.Lazy(true)); + + rc.ManyToOne(ep => ep.Child1, m => m.Column("Child1Id")); + rc.ManyToOne(ep => ep.Child2, m => m.Column("Child2Id")); + rc.ManyToOne(ep => ep.SameTypeChild, m => m.Column("SameTypeChildId")); + + rc.Bag( + ep => ep.ChildrenList, + m => + { + m.Cascade(Mapping.ByCode.Cascade.All); + m.Inverse(true); + }, + a => a.OneToMany()); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Parent); + rc.Property(x => x.Name); + }); + mapper.Class( + rc => + { + rc.Lazy(false); + + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + + rc.Bag(ep => ep.ChildrenListSubselect, + m => + { + m.Cascade(Mapping.ByCode.Cascade.All); + m.Inverse(true); + m.Fetch(CollectionFetchMode.Subselect); + m.Lazy(CollectionLazy.NoLazy); + }, + a => a.OneToMany()); + + rc.Bag(ep => ep.ChildrenListEager, + m => + { + m.Lazy(CollectionLazy.NoLazy); + }, + a => a.OneToMany()); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(c => c.Parent); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var child1 = new EntitySimpleChild + { + Name = "Child1", + }; + var child2 = new EntitySimpleChild + { + Name = "Child2" + }; + var complex = new EntityComplex + { + Name = "ComplexEnityParent", + Child1 = child1, + Child2 = child2, + LazyProp = "SomeBigValue", + SameTypeChild = new EntityComplex() + { + Name = "ComplexEntityChild" + }, + }; + child1.Parent = child2.Parent = complex; + + var eager = new EntityEager() + { + Name = "eager1", + }; + + var eager2 = new EntityEager() + { + Name = "eager2", + }; + eager.ChildrenListSubselect = new List() + { + new EntitySubselectChild() + { + Name = "subselect1", + Parent = eager, + }, + new EntitySubselectChild() + { + Name = "subselect2", + Parent = eager, + }, + }; + + session.Save(child1); + session.Save(child2); + session.Save(complex.SameTypeChild); + session.Save(complex); + session.Save(eager); + session.Save(eager2); + + session.Flush(); + transaction.Commit(); + + _parentId = complex.Id; + _eagerId = eager.Id; + } + } + + public class ListTransformerToInt : IResultTransformer + { + public object TransformTuple(object[] tuple, string[] aliases) + { + return tuple.Length == 1 ? tuple[0] : tuple; + } + + public IList TransformList(IList collection) + { + return new List() + { + 1, + 2, + 3, + 4, + }; + } + } + + private bool SupportsMultipleQueries => Sfi.ConnectionProvider.Driver.SupportsMultipleQueries; + + #endregion Test Setup + } +} diff --git a/src/NHibernate.Test/Legacy/MultiTableTest.cs b/src/NHibernate.Test/Legacy/MultiTableTest.cs index e17561d202b..835c808ab31 100644 --- a/src/NHibernate.Test/Legacy/MultiTableTest.cs +++ b/src/NHibernate.Test/Legacy/MultiTableTest.cs @@ -5,6 +5,8 @@ using NHibernate.DomainModel; using NUnit.Framework; +using MultiEntity = NHibernate.DomainModel.Multi; + namespace NHibernate.Test.Legacy { /// @@ -208,7 +210,7 @@ public void MultiTable() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -257,7 +259,7 @@ public void MultiTable() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) s.Load(typeof(Multi), mid); + multi = (MultiEntity) s.Load(typeof(MultiEntity), mid); Assert.AreEqual("extra2", multi.ExtraProp); multi.ExtraProp = multi.ExtraProp + "3"; Assert.AreEqual("new name", multi.Name); @@ -270,9 +272,9 @@ public void MultiTable() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) s.Load(typeof(Top), mid); + multi = (MultiEntity) s.Load(typeof(Top), mid); simp = (Top) s.Load(typeof(Top), sid); - Assert.IsFalse(simp is Multi); + Assert.IsFalse(simp is MultiEntity); Assert.AreEqual("extra23", multi.ExtraProp); Assert.AreEqual("newer name", multi.Name); t.Commit(); @@ -287,8 +289,8 @@ public void MultiTable() while (enumer.MoveNext()) { object o = enumer.Current; - if ((o is Top) && !(o is Multi)) foundSimp = true; - if ((o is Multi) && !(o is SubMulti)) foundMulti = true; + if ((o is Top) && !(o is MultiEntity)) foundSimp = true; + if ((o is MultiEntity) && !(o is SubMulti)) foundMulti = true; if (o is SubMulti) foundSubMulti = true; } Assert.IsTrue(foundSimp); @@ -323,7 +325,7 @@ public void MultiTable() s = OpenSession(); t = s.BeginTransaction(); if (TestDialect.SupportsSelectForUpdateOnOuterJoin) - multi = (Multi)s.Load(typeof(Top), mid, LockMode.Upgrade); + multi = (MultiEntity)s.Load(typeof(Top), mid, LockMode.Upgrade); simp = (Top) s.Load(typeof(Top), sid); s.Lock(simp, LockMode.UpgradeNoWait); t.Commit(); @@ -343,7 +345,7 @@ public void MultiTableGeneratedId() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -371,7 +373,7 @@ public void MultiTableGeneratedId() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) s.Load(typeof(Multi), multiId); + multi = (MultiEntity) s.Load(typeof(MultiEntity), multiId); Assert.AreEqual("extra2", multi.ExtraProp); multi.ExtraProp += "3"; Assert.AreEqual("new name", multi.Name); @@ -384,11 +386,9 @@ public void MultiTableGeneratedId() s = OpenSession(); t = s.BeginTransaction(); - multi = (Multi) s.Load(typeof(Top), multiId); + multi = (MultiEntity) s.Load(typeof(Top), multiId); simp = (Top) s.Load(typeof(Top), simpId); - Assert.IsFalse(simp is Multi); - // Can't see the point of this test since the variable is declared as Multi! - //Assert.IsTrue( multi is Multi ); + Assert.IsFalse(simp is MultiEntity); Assert.AreEqual("extra23", multi.ExtraProp); Assert.AreEqual("newer name", multi.Name); t.Commit(); @@ -403,8 +403,8 @@ public void MultiTableGeneratedId() foreach (object obj in enumer) { - if ((obj is Top) && !(obj is Multi)) foundSimp = true; - if ((obj is Multi) && !(obj is SubMulti)) foundMulti = true; + if ((obj is Top) && !(obj is MultiEntity)) foundSimp = true; + if ((obj is MultiEntity) && !(obj is SubMulti)) foundMulti = true; if (obj is SubMulti) foundSubMulti = true; } Assert.IsTrue(foundSimp); @@ -431,7 +431,7 @@ public void MultiTableGeneratedId() s = OpenSession(); t = s.BeginTransaction(); if (TestDialect.SupportsSelectForUpdateOnOuterJoin) - multi = (Multi) s.Load(typeof(Top), multiId, LockMode.Upgrade); + multi = (MultiEntity) s.Load(typeof(Top), multiId, LockMode.Upgrade); simp = (Top) s.Load(typeof(Top), simpId); s.Lock(simp, LockMode.UpgradeNoWait); t.Commit(); @@ -457,7 +457,7 @@ public void MultiTableCollections() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); Assert.AreEqual(0, s.CreateQuery("from s in class Top").List().Count); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -514,7 +514,7 @@ public void MultiTableCollections() foreach (object obj in ls.Set) { if (obj is Top) foundSimple++; - if (obj is Multi) foundMulti++; + if (obj is MultiEntity) foundMulti++; } Assert.AreEqual(2, foundSimple); Assert.AreEqual(1, foundMulti); @@ -534,7 +534,7 @@ public void MultiTableManyToOne() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); Assert.AreEqual(0, s.CreateQuery("from s in class Top").List().Count); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; multi.Name = "name"; Top simp = new Top(); @@ -579,7 +579,7 @@ public void MultiTableManyToOne() Assert.AreSame(ls, ls.Other); Assert.AreSame(ls, ls.YetAnother); Assert.AreEqual("name", ls.Another.Name); - Assert.IsTrue(ls.Another is Multi); + Assert.IsTrue(ls.Another is MultiEntity); s.Delete(ls); s.Delete(ls.Another); t.Commit(); @@ -591,7 +591,7 @@ public void MultiTableNativeId() { ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi = new Multi(); + MultiEntity multi = new MultiEntity(); multi.ExtraProp = "extra"; object id = s.Save(multi); Assert.IsNotNull(id); @@ -608,14 +608,14 @@ public void Collection() ISession s = OpenSession(); ITransaction t = s.BeginTransaction(); - Multi multi1 = new Multi(); + MultiEntity multi1 = new MultiEntity(); multi1.ExtraProp = "extra1"; - Multi multi2 = new Multi(); + MultiEntity multi2 = new MultiEntity(); multi2.ExtraProp = "extra2"; Po po = new Po(); multi1.Po = po; multi2.Po = po; - po.Set = new HashSet {multi1, multi2}; + po.Set = new HashSet {multi1, multi2}; po.List = new List {new SubMulti()}; object id = s.Save(po); Assert.IsNotNull(id); diff --git a/src/NHibernate.Test/Linq/ExpressionSessionLeakTest.cs b/src/NHibernate.Test/Linq/ExpressionSessionLeakTest.cs index 35990bdf34b..baaaaec7f2c 100644 --- a/src/NHibernate.Test/Linq/ExpressionSessionLeakTest.cs +++ b/src/NHibernate.Test/Linq/ExpressionSessionLeakTest.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Reflection; using NHibernate.DomainModel.Northwind.Entities; using NHibernate.Linq; using NUnit.Framework; @@ -38,10 +37,6 @@ private WeakReference DoLinqInSeparateSession() } } - static readonly PropertyInfo SessionProperty = typeof(DefaultQueryProvider).GetProperty( - "Session", - BindingFlags.NonPublic | BindingFlags.Instance); - [Theory] public void SessionIsNotNullOrResurrected(bool? disposeSession) { @@ -50,7 +45,6 @@ public void SessionIsNotNullOrResurrected(bool? disposeSession) if (provider == null) Assert.Ignore("Another query provider than NHibernate default one is used"); - Assert.That(SessionProperty, Is.Not.Null, $"Session property on {nameof(DefaultQueryProvider)} is not found."); // Force collection of no more strongly referenced objects. // Do not wait for pending finalizers @@ -58,12 +52,12 @@ public void SessionIsNotNullOrResurrected(bool? disposeSession) try { - var s = SessionProperty.GetValue(provider); + var s = provider.Session; Assert.Fail($"Getting provider Session property did not failed. Obtained {(s == null ? "null" : s.GetType().Name)}."); } - catch (TargetInvocationException tie) + catch (Exception e) { - Assert.That(tie.InnerException, Is.TypeOf().And.Message.Contains("garbage coll")); + Assert.That(e, Is.TypeOf().And.Message.Contains("garbage coll")); } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH1989/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1989/Fixture.cs index 29ca75c4e3e..7b1ac3f7be7 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH1989/Fixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH1989/Fixture.cs @@ -151,7 +151,6 @@ public void SecondLevelCacheWithMixedCacheableAndNonCacheableFuture() .SetCacheable(true) .FutureValue(); - // non cacheable Future causes batch to be non-cacheable int count = s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -178,7 +177,9 @@ public void SecondLevelCacheWithMixedCacheableAndNonCacheableFuture() .FutureValue() .Value; - Assert.That(userFuture.Value, Is.Null, + Assert.That(userFuture.Value, Is.Not.Null, + "query results should come from cache"); + Assert.That(count, Is.EqualTo(0), "query results should not come from cache"); } } @@ -204,7 +205,6 @@ public void SecondLevelCacheWithMixedCacheRegionsFuture() .SetCacheRegion("region1") .FutureValue(); - // different cache-region causes batch to be non-cacheable int count = s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -228,6 +228,13 @@ public void SecondLevelCacheWithMixedCacheRegionsFuture() .SetCacheRegion("region1") .FutureValue(); + IFutureValue userFutureWrongRegion = + s.CreateCriteria() + .Add(Restrictions.NaturalId().Set("Name", "test")) + .SetCacheable(true) + .SetCacheRegion("region2") + .FutureValue(); + int count = s.CreateCriteria() .SetProjection(Projections.RowCount()) @@ -236,8 +243,23 @@ public void SecondLevelCacheWithMixedCacheRegionsFuture() .FutureValue() .Value; - Assert.That(userFuture.Value, Is.Null, - "query results should not come from cache"); + int countWrongRegion = + s.CreateCriteria() + .SetProjection(Projections.RowCount()) + .SetCacheable(true) + .SetCacheRegion("region1") + .FutureValue() + .Value; + + Assert.That(userFuture.Value, Is.Not.Null, + "query results should come from cache"); + Assert.That(count, Is.EqualTo(1), + "query results should come from cache"); + + Assert.That(userFutureWrongRegion.Value, Is.Null, + "query results from wrong cache region"); + Assert.That(countWrongRegion, Is.EqualTo(0), + "query results from wrong cache region"); } } diff --git a/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs b/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs index cabf6d1ddbf..dc640c58c38 100644 --- a/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs +++ b/src/NHibernate.Test/QueryTest/MultiCriteriaFixture.cs @@ -520,5 +520,32 @@ public void UsingManyParametersAndQueries_DoesNotCauseParameterNameCollisions() } } } + + //NH-2428 - Session.MultiCriteria and FlushMode.Auto inside transaction (GH865) + [Test] + public void MultiCriteriaAutoFlush() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Auto; + var p1 = new Item + { + Name = "Person name", + Id = 15 + }; + s.Save(p1); + s.Flush(); + + s.Delete(p1); + var multi = s.CreateMultiCriteria(); + multi.Add(s.QueryOver().ToRowCountQuery()); + var count = (int) ((IList) multi.List()[0])[0]; + tx.Commit(); + + Assert.That(count, Is.EqualTo(0), "Session wasn't auto flushed."); + + } + } } } diff --git a/src/NHibernate/Async/Engine/ISessionImplementor.cs b/src/NHibernate/Async/Engine/ISessionImplementor.cs index ee08870c55d..d5108c3e8f8 100644 --- a/src/NHibernate/Async/Engine/ISessionImplementor.cs +++ b/src/NHibernate/Async/Engine/ISessionImplementor.cs @@ -20,14 +20,29 @@ using NHibernate.Hql; using NHibernate.Impl; using NHibernate.Loader.Custom; +using NHibernate.Multi; using NHibernate.Persister.Entity; using NHibernate.Transaction; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate.Engine { using System.Threading.Tasks; using System.Threading; + internal static partial class SessionImplementorExtensions + { + + internal static async Task AutoFlushIfRequiredAsync(this ISessionImplementor implementor, ISet querySpaces, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var autoFlushIfRequiredTask = (implementor as AbstractSessionImpl)?.AutoFlushIfRequiredAsync(querySpaces, cancellationToken); + if (autoFlushIfRequiredTask != null) + { + await (autoFlushIfRequiredTask).ConfigureAwait(false); + } + } + } public partial interface ISessionImplementor { @@ -164,6 +179,8 @@ public partial interface ISessionImplementor Task> ListCustomQueryAsync(ICustomQuery customQuery, QueryParameters queryParameters, CancellationToken cancellationToken); + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] Task GetQueriesAsync(IQueryExpression query, bool scalar, CancellationToken cancellationToken); // NH specific for MultiQuery /// diff --git a/src/NHibernate/Async/ISession.cs b/src/NHibernate/Async/ISession.cs index 45cc1b9f9e3..fbe6f6d19a6 100644 --- a/src/NHibernate/Async/ISession.cs +++ b/src/NHibernate/Async/ISession.cs @@ -17,8 +17,10 @@ using NHibernate.Event; using NHibernate.Event.Default; using NHibernate.Impl; +using NHibernate.Multi; using NHibernate.Stat; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate { diff --git a/src/NHibernate/Async/IStatelessSession.cs b/src/NHibernate/Async/IStatelessSession.cs index 90775b18b86..5e86ca768c8 100644 --- a/src/NHibernate/Async/IStatelessSession.cs +++ b/src/NHibernate/Async/IStatelessSession.cs @@ -14,11 +14,15 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Engine; +using NHibernate.Impl; +using NHibernate.Multi; +using NHibernate.Util; namespace NHibernate { using System.Threading.Tasks; using System.Threading; + public partial interface IStatelessSession : IDisposable { diff --git a/src/NHibernate/Async/Impl/AbstractQueryImpl.cs b/src/NHibernate/Async/Impl/AbstractQueryImpl.cs index 5740db48210..cf86df99324 100644 --- a/src/NHibernate/Async/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Async/Impl/AbstractQueryImpl.cs @@ -14,16 +14,17 @@ using NHibernate.Engine; using NHibernate.Engine.Query; using NHibernate.Hql; +using NHibernate.Multi; using NHibernate.Proxy; using NHibernate.Transform; using NHibernate.Type; using NHibernate.Util; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Impl { - using System.Threading.Tasks; - using System.Threading; public abstract partial class AbstractQueryImpl : IQuery { @@ -56,7 +57,5 @@ public abstract partial class AbstractQueryImpl : IQuery } #endregion - - protected internal abstract Task> GetTranslatorsAsync(ISessionImplementor sessionImplementor, QueryParameters queryParameters, CancellationToken cancellationToken); } } diff --git a/src/NHibernate/Async/Impl/AbstractQueryImpl2.cs b/src/NHibernate/Async/Impl/AbstractQueryImpl2.cs index ab86dcd51e9..00fad92ccd3 100644 --- a/src/NHibernate/Async/Impl/AbstractQueryImpl2.cs +++ b/src/NHibernate/Async/Impl/AbstractQueryImpl2.cs @@ -16,6 +16,7 @@ namespace NHibernate.Impl { + using System; using System.Threading.Tasks; using System.Threading; public abstract partial class AbstractQueryImpl2 : AbstractQueryImpl @@ -117,14 +118,22 @@ public abstract partial class AbstractQueryImpl2 : AbstractQueryImpl } } - protected internal override async Task> GetTranslatorsAsync(ISessionImplementor sessionImplementor, QueryParameters queryParameters, CancellationToken cancellationToken) + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] + protected internal override Task> GetTranslatorsAsync(ISessionImplementor sessionImplementor, QueryParameters queryParameters, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - // NOTE: updates queryParameters.NamedParameters as (desired) side effect - var queryExpression = ExpandParameters(queryParameters.NamedParameters); - - return (await (sessionImplementor.GetQueriesAsync(queryExpression, false, cancellationToken)).ConfigureAwait(false)) - .Select(queryTranslator => new HqlTranslatorWrapper(queryTranslator)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + try + { + return Task.FromResult>(GetTranslators(sessionImplementor, queryParameters)); + } + catch (System.Exception ex) + { + return Task.FromException>(ex); + } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs index 95a6fd28a4d..2d28c429495 100644 --- a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs @@ -26,6 +26,7 @@ using NHibernate.Linq; using NHibernate.Loader.Custom; using NHibernate.Loader.Custom.Sql; +using NHibernate.Multi; using NHibernate.Persister.Entity; using NHibernate.Transaction; using NHibernate.Type; @@ -160,10 +161,36 @@ public virtual async Task> ListCustomQueryAsync(ICustomQuery customQ } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public abstract Task GetQueriesAsync(IQueryExpression query, bool scalar, CancellationToken cancellationToken); public abstract Task GetEntityUsingInterceptorAsync(EntityKey key, CancellationToken cancellationToken); public abstract Task ExecuteNativeUpdateAsync(NativeSQLQuerySpecification specification, QueryParameters queryParameters, CancellationToken cancellationToken); + //6.0 TODO: Make abstract + /// + /// detect in-memory changes, determine if the changes are to tables + /// named in the query and, if so, complete execution the flush + /// + /// + /// A cancellation token that can be used to cancel the work + /// Returns true if flush was executed + public virtual Task AutoFlushIfRequiredAsync(ISet querySpaces, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(AutoFlushIfRequired(querySpaces)); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + public abstract Task FlushAsync(CancellationToken cancellationToken); #endregion diff --git a/src/NHibernate/Async/Impl/CriteriaImpl.cs b/src/NHibernate/Async/Impl/CriteriaImpl.cs index 7f8ed8e184f..81e735bc052 100644 --- a/src/NHibernate/Async/Impl/CriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/CriteriaImpl.cs @@ -14,6 +14,7 @@ using System.Text; using NHibernate.Criterion; using NHibernate.Engine; +using NHibernate.Multi; using NHibernate.SqlCommand; using NHibernate.Transform; using NHibernate.Util; diff --git a/src/NHibernate/Async/Impl/FutureCriteriaBatch.cs b/src/NHibernate/Async/Impl/FutureCriteriaBatch.cs index baaedfadf5d..7319492da6f 100644 --- a/src/NHibernate/Async/Impl/FutureCriteriaBatch.cs +++ b/src/NHibernate/Async/Impl/FutureCriteriaBatch.cs @@ -8,6 +8,7 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; namespace NHibernate.Impl diff --git a/src/NHibernate/Async/Impl/FutureQueryBatch.cs b/src/NHibernate/Async/Impl/FutureQueryBatch.cs index d15877a05dc..b43e697e08f 100644 --- a/src/NHibernate/Async/Impl/FutureQueryBatch.cs +++ b/src/NHibernate/Async/Impl/FutureQueryBatch.cs @@ -8,6 +8,7 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; using NHibernate.Transform; diff --git a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs index 2d9f51ebe1a..f40733eedc5 100644 --- a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs @@ -12,6 +12,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using NHibernate.Cache; using NHibernate.Criterion; using NHibernate.Driver; @@ -49,9 +50,14 @@ public partial class MultiCriteriaImpl : IMultiCriteria } } + var querySpaces = new HashSet(loaders.SelectMany(l => l.QuerySpaces)); + if (resultSetsCommand.HasQueries) + { + await (session.AutoFlushIfRequiredAsync(querySpaces, cancellationToken)).ConfigureAwait(false); + } if (cacheable) { - criteriaResults = await (ListUsingQueryCacheAsync(cancellationToken)).ConfigureAwait(false); + criteriaResults = await (ListUsingQueryCacheAsync(querySpaces, cancellationToken)).ConfigureAwait(false); } else { @@ -62,20 +68,18 @@ public partial class MultiCriteriaImpl : IMultiCriteria } } - private async Task ListUsingQueryCacheAsync(CancellationToken cancellationToken) + private async Task ListUsingQueryCacheAsync(HashSet querySpaces, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); IQueryCache queryCache = session.Factory.GetQueryCache(cacheRegion); ISet filterKeys = FilterKey.CreateFilterKeys(session.EnabledFilters); - ISet querySpaces = new HashSet(); List resultTypesList = new List(); int[] maxRows = new int[loaders.Count]; int[] firstRows = new int[loaders.Count]; for (int i = 0; i < loaders.Count; i++) { - querySpaces.UnionWith(loaders[i].QuerySpaces); resultTypesList.Add(loaders[i].ResultTypes); firstRows[i] = parameters[i].RowSelection.FirstRow; maxRows[i] = parameters[i].RowSelection.MaxRows; diff --git a/src/NHibernate/Async/Impl/MultiQueryImpl.cs b/src/NHibernate/Async/Impl/MultiQueryImpl.cs index 6f3519d2351..35a964ca0c3 100644 --- a/src/NHibernate/Async/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Async/Impl/MultiQueryImpl.cs @@ -12,6 +12,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using NHibernate.Cache; using NHibernate.Driver; using NHibernate.Engine; @@ -56,7 +57,14 @@ public partial class MultiQueryImpl : IMultiQuery try { Before(); - return cacheable ? await (ListUsingQueryCacheAsync(cancellationToken)).ConfigureAwait(false) : await (ListIgnoreQueryCacheAsync(cancellationToken)).ConfigureAwait(false); + + var querySpaces = new HashSet(Translators.SelectMany(t => t.QuerySpaces)); + if (resultSetsCommand.HasQueries) + { + await (session.AutoFlushIfRequiredAsync(querySpaces, cancellationToken)).ConfigureAwait(false); + } + + return cacheable ? await (ListUsingQueryCacheAsync(querySpaces, cancellationToken)).ConfigureAwait(false) : await (ListIgnoreQueryCacheAsync(cancellationToken)).ConfigureAwait(false); } finally { @@ -188,27 +196,6 @@ protected async Task> DoListAsync(CancellationToken cancellationTok return results; } - private async Task AggregateQueriesInformationAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - int queryIndex = 0; - foreach (AbstractQueryImpl query in queries) - { - query.VerifyParameters(); - QueryParameters queryParameters = query.GetQueryParameters(); - queryParameters.ValidateParameters(); - foreach (var translator in await (query.GetTranslatorsAsync(session, queryParameters, cancellationToken)).ConfigureAwait(false)) - { - translators.Add(translator); - translatorQueryMap.Add(queryIndex); - parameters.Add(queryParameters); - ISqlCommand singleCommand = translator.Loader.CreateSqlCommand(queryParameters, session); - resultSetsCommand.Append(singleCommand); - } - queryIndex++; - } - } - public async Task GetResultAsync(string key, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -232,19 +219,17 @@ private async Task ListIgnoreQueryCacheAsync(CancellationToken cancellati return GetResultList(await (DoListAsync(cancellationToken)).ConfigureAwait(false)); } - private async Task ListUsingQueryCacheAsync(CancellationToken cancellationToken) + private async Task ListUsingQueryCacheAsync(HashSet querySpaces, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); IQueryCache queryCache = session.Factory.GetQueryCache(cacheRegion); ISet filterKeys = FilterKey.CreateFilterKeys(session.EnabledFilters); - ISet querySpaces = new HashSet(); List resultTypesList = new List(Translators.Count); for (int i = 0; i < Translators.Count; i++) { ITranslator queryTranslator = Translators[i]; - querySpaces.UnionWith(queryTranslator.QuerySpaces); resultTypesList.Add(queryTranslator.ReturnTypes); } int[] firstRows = new int[Parameters.Count]; diff --git a/src/NHibernate/Async/Impl/SessionImpl.cs b/src/NHibernate/Async/Impl/SessionImpl.cs index dfe0886d1ca..64ee56a2ee4 100644 --- a/src/NHibernate/Async/Impl/SessionImpl.cs +++ b/src/NHibernate/Async/Impl/SessionImpl.cs @@ -286,6 +286,8 @@ private async Task ListAsync(IQueryExpression queryExpression, QueryParameters q } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public override async Task GetQueriesAsync(IQueryExpression query, bool scalar, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -677,8 +679,8 @@ public override async Task GetEntityUsingInterceptorAsync(EntityKey key, /// /// /// A cancellation token that can be used to cancel the work - /// - private async Task AutoFlushIfRequiredAsync(ISet querySpaces, CancellationToken cancellationToken) + /// Returns true if flush was executed + public override async Task AutoFlushIfRequiredAsync(ISet querySpaces, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (BeginProcess()) diff --git a/src/NHibernate/Async/Impl/SqlQueryImpl.cs b/src/NHibernate/Async/Impl/SqlQueryImpl.cs index 6811ce34792..31b2edb0dc7 100644 --- a/src/NHibernate/Async/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Async/Impl/SqlQueryImpl.cs @@ -106,6 +106,8 @@ public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizabl } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] protected internal override Task> GetTranslatorsAsync(ISessionImplementor sessionImplementor, QueryParameters queryParameters, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs index 1b41790b99b..b713f0b5cf7 100644 --- a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs @@ -277,6 +277,8 @@ public override async Task ListCustomQueryAsync(ICustomQuery customQuery, QueryP } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public override Task GetQueriesAsync(IQueryExpression query, bool scalar, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/NHibernate/Async/Linq/DefaultQueryProvider.cs b/src/NHibernate/Async/Linq/DefaultQueryProvider.cs index e133891a4ef..f3c138f3511 100644 --- a/src/NHibernate/Async/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Async/Linq/DefaultQueryProvider.cs @@ -20,6 +20,7 @@ using NHibernate.Type; using NHibernate.Util; using System.Threading.Tasks; +using NHibernate.Multi; namespace NHibernate.Linq { @@ -28,7 +29,7 @@ public partial interface INhQueryProvider : IQueryProvider Task ExecuteDmlAsync(QueryMode queryMode, Expression expression, CancellationToken cancellationToken); } - public partial class DefaultQueryProvider : INhQueryProvider, IQueryProviderWithOptions + public partial class DefaultQueryProvider : INhQueryProvider, IQueryProviderWithOptions, ISupportFutureBatchNhQueryProvider { // Since v5.1 diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 3915e725eec..c18cbca0a7c 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -1173,7 +1173,7 @@ protected Task ListAsync(ISessionImplementor session, QueryParameters que } try { - bool cacheable = _factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable; + var cacheable = IsCacheable(queryParameters); if (cacheable) { @@ -1208,23 +1208,12 @@ private async Task ListUsingQueryCacheAsync(ISessionImplementor session, await (PutResultInQueryCacheAsync(session, queryParameters, queryCache, key, result, cancellationToken)).ConfigureAwait(false); } - IResultTransformer resolvedTransformer = ResolveResultTransformer(queryParameters.ResultTransformer); - if (resolvedTransformer != null) - { - result = (AreResultSetRowsTransformedImmediately() - ? key.ResultTransformer.RetransformResults( - result, - ResultRowAliases, - queryParameters.ResultTransformer, - IncludeInResultRow) - : key.ResultTransformer.UntransformToTuples(result) - ); - } + result = TransformCacheableResults(queryParameters, key.ResultTransformer, result); return GetResultList(result, queryParameters.ResultTransformer); } - private async Task GetResultFromQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, + internal async Task GetResultFromQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, ISet querySpaces, IQueryCache queryCache, QueryKey key, CancellationToken cancellationToken) { @@ -1269,7 +1258,7 @@ private async Task GetResultFromQueryCacheAsync(ISessionImplementor sessi return result; } - private async Task PutResultInQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, + internal async Task PutResultInQueryCacheAsync(ISessionImplementor session, QueryParameters queryParameters, IQueryCache queryCache, QueryKey key, IList result, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Multi/CriteriaBatchItem.cs b/src/NHibernate/Async/Multi/CriteriaBatchItem.cs new file mode 100644 index 00000000000..2e812ec87ee --- /dev/null +++ b/src/NHibernate/Async/Multi/CriteriaBatchItem.cs @@ -0,0 +1,33 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using NHibernate.Impl; +using NHibernate.Loader.Criteria; +using NHibernate.Persister.Entity; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + public partial class CriteriaBatchItem : QueryBatchItemBase + { + + protected override Task> GetResultsNonBatchedAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + return _criteria.ListAsync(cancellationToken); + } + } +} diff --git a/src/NHibernate/Async/Multi/IQueryBatch.cs b/src/NHibernate/Async/Multi/IQueryBatch.cs new file mode 100644 index 00000000000..85bdc444411 --- /dev/null +++ b/src/NHibernate/Async/Multi/IQueryBatch.cs @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + public partial interface IQueryBatch + { + /// + /// Executes the batch. + /// + /// A cancellation token that can be used to cancel the work + Task ExecuteAsync(CancellationToken cancellationToken); + + /// + /// Gets a query result, triggering execution of the batch if it was not already executed. + /// + /// The index of the query for which results are to be obtained. + /// A cancellation token that can be used to cancel the work + /// The type of the result elements of the query. + /// A query result. + /// is 0 based and matches the order in which queries have been + /// added into the batch. + Task> GetResultAsync(int queryIndex, CancellationToken cancellationToken); + + /// + /// Gets a query result, triggering execution of the batch if it was not already executed. + /// + /// The key of the query for which results are to be obtained. + /// A cancellation token that can be used to cancel the work + /// The type of the result elements of the query. + /// A query result. + Task> GetResultAsync(string querykey, CancellationToken cancellationToken); + } +} diff --git a/src/NHibernate/Async/Multi/IQueryBatchItem.cs b/src/NHibernate/Async/Multi/IQueryBatchItem.cs new file mode 100644 index 00000000000..876f9618cdd --- /dev/null +++ b/src/NHibernate/Async/Multi/IQueryBatchItem.cs @@ -0,0 +1,43 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Data.Common; +using NHibernate.Engine; +using NHibernate.SqlCommand; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + + public partial interface IQueryBatchItem + { + + /// + /// Returns commands generated by query + /// + /// A cancellation token that can be used to cancel the work + Task> GetCommandsAsync(CancellationToken cancellationToken); + + /// + /// Executed after all commands in batch are processed + /// + /// A cancellation token that can be used to cancel the work + Task ProcessResultsAsync(CancellationToken cancellationToken); + + /// + /// Immediate query execution in case the dialect does not support batches + /// + /// A cancellation token that can be used to cancel the work + Task ExecuteNonBatchedAsync(CancellationToken cancellationToken); + } +} diff --git a/src/NHibernate/Async/Multi/LinqBatchItem.cs b/src/NHibernate/Async/Multi/LinqBatchItem.cs new file mode 100644 index 00000000000..638948f3cf5 --- /dev/null +++ b/src/NHibernate/Async/Multi/LinqBatchItem.cs @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Linq; +using NHibernate.Util; +using Remotion.Linq.Parsing.ExpressionVisitors; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + + public partial class LinqBatchItem : QueryBatchItem + { + + protected override async Task> GetResultsNonBatchedAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_postExecuteTransformer == null) + { + return await (base.GetResultsNonBatchedAsync(cancellationToken)).ConfigureAwait(false); + } + + return GetTransformedResults(await (Query.ListAsync(cancellationToken)).ConfigureAwait(false)); + } + } +} diff --git a/src/NHibernate/Async/Multi/QueryBatch.cs b/src/NHibernate/Async/Multi/QueryBatch.cs new file mode 100644 index 00000000000..55ddc679111 --- /dev/null +++ b/src/NHibernate/Async/Multi/QueryBatch.cs @@ -0,0 +1,167 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NHibernate.Driver; +using NHibernate.Engine; +using NHibernate.Exceptions; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + public partial class QueryBatch : IQueryBatch + { + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_queries.Count == 0) + return; + var sessionFlushMode = Session.FlushMode; + if (FlushMode.HasValue) + Session.FlushMode = FlushMode.Value; + try + { + Init(); + + if (!Session.Factory.ConnectionProvider.Driver.SupportsMultipleQueries) + { + foreach (var query in _queries) + { + await (query.ExecuteNonBatchedAsync(cancellationToken)).ConfigureAwait(false); + } + return; + } + + using (Session.BeginProcess()) + { + await (DoExecuteAsync(cancellationToken)).ConfigureAwait(false); + } + } + finally + { + if (_autoReset) + { + _queries.Clear(); + _queriesByKey.Clear(); + } + else + _executed = true; + + if (FlushMode.HasValue) + Session.FlushMode = sessionFlushMode; + } + } + + /// + public Task> GetResultAsync(int queryIndex, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + return GetResultsAsync(_queries[queryIndex], cancellationToken); + } + + /// + public Task> GetResultAsync(string querykey, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + return GetResultsAsync(_queriesByKey[querykey], cancellationToken); + } + + private async Task> GetResultsAsync(IQueryBatchItem query, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!_executed) + await (ExecuteAsync(cancellationToken)).ConfigureAwait(false); + 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 DoExecuteAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var resultSetsCommand = Session.Factory.ConnectionProvider.Driver.GetResultSetsCommand(Session); + await (CombineQueriesAsync(resultSetsCommand, cancellationToken)).ConfigureAwait(false); + + var querySpaces = new HashSet(_queries.SelectMany(t => t.GetQuerySpaces())); + if (resultSetsCommand.HasQueries) + { + await (Session.AutoFlushIfRequiredAsync(querySpaces, cancellationToken)).ConfigureAwait(false); + } + + bool statsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + Stopwatch stopWatch = null; + if (statsEnabled) + { + stopWatch = new Stopwatch(); + stopWatch.Start(); + } + if (Log.IsDebugEnabled()) + { + Log.Debug("Multi query with {0} queries: {1}", _queries.Count, resultSetsCommand.Sql); + } + + int rowCount = 0; + try + { + if (resultSetsCommand.HasQueries) + { + using (var reader = await (resultSetsCommand.GetReaderAsync(Timeout, cancellationToken)).ConfigureAwait(false)) + { + foreach (var multiSource in _queries) + { + foreach (var resultSetHandler in multiSource.GetResultSetHandler()) + { + rowCount += resultSetHandler(reader); + await (reader.NextResultAsync(cancellationToken)).ConfigureAwait(false); + } + } + } + } + + foreach (var multiSource in _queries) + { + await (multiSource.ProcessResultsAsync(cancellationToken)).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { throw; } + catch (Exception sqle) + { + Log.Error(sqle, "Failed to execute multi query: [{0}]", resultSetsCommand.Sql); + throw ADOExceptionHelper.Convert(Session.Factory.SQLExceptionConverter, sqle, "Failed to execute multi query", resultSetsCommand.Sql); + } + + if (statsEnabled) + { + stopWatch.Stop(); + Session.Factory.StatisticsImplementor.QueryExecuted($"{_queries.Count} queries", rowCount, stopWatch.Elapsed); + } + } + } +} diff --git a/src/NHibernate/Async/Multi/QueryBatchExtensions.cs b/src/NHibernate/Async/Multi/QueryBatchExtensions.cs new file mode 100644 index 00000000000..5786847c97e --- /dev/null +++ b/src/NHibernate/Async/Multi/QueryBatchExtensions.cs @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using NHibernate.Criterion; +using NHibernate.Engine; + +namespace NHibernate.Multi +{ + public static partial class QueryBatchExtensions + { + + #region Helper classes + + partial class FutureValue : IFutureValue + { + + public async Task GetValueAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_futureList == null) + return _result; + + _result = (await (_futureList.GetValueAsync(cancellationToken)).ConfigureAwait(false)).FirstOrDefault(); + _futureList = null; + + return _result; + } + } + + partial class FutureList : IFutureList + { + + public async Task> GetValueAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_batch == null) + return _list; + + if (!_batch.IsExecutedOrEmpty) + await (_batch.ExecuteAsync(cancellationToken)).ConfigureAwait(false); + _list = _query.GetResults(); + + _batch = null; + _query = null; + + return _list; + } + } + + #endregion Helper classes + } +} diff --git a/src/NHibernate/Async/Multi/QueryBatchItem.cs b/src/NHibernate/Async/Multi/QueryBatchItem.cs new file mode 100644 index 00000000000..c1592e29031 --- /dev/null +++ b/src/NHibernate/Async/Multi/QueryBatchItem.cs @@ -0,0 +1,33 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Impl; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + public partial class QueryBatchItem : QueryBatchItemBase + { + + protected override Task> GetResultsNonBatchedAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + return Query.ListAsync(cancellationToken); + } + } +} diff --git a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs new file mode 100644 index 00000000000..ff2d884ce18 --- /dev/null +++ b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Engine; +using NHibernate.SqlCommand; +using NHibernate.Util; + +namespace NHibernate.Multi +{ + using System.Threading.Tasks; + using System.Threading; + public abstract partial class QueryBatchItemBase : IQueryBatchItem + { + + /// + /// Gets the commands to execute for getting the not-already cached results of this query. Does retrieves + /// already cached results by side-effect. + /// + /// A cancellation token that can be used to cancel the work + /// The commands for obtaining the results not already cached. + 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)) + { + // 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. + qi.Cache = null; + _loaderResults[index] = resultsFromCache; + continue; + } + } + yields.Add(qi.Loader.CreateSqlCommand(qi.Parameters, Session)); + } + return yields; + } + + public async Task ProcessResultsAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + for (int 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.Cache != null) + { + await (queryInfo.Loader.PutResultInQueryCacheAsync(Session, queryInfo.Parameters, queryInfo.Cache, queryInfo.CacheKey, _loaderResults[i], cancellationToken)).ConfigureAwait(false); + } + } + AfterLoadCallback?.Invoke(GetResults()); + } + + public async Task ExecuteNonBatchedAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + _finalResults = await (GetResultsNonBatchedAsync(cancellationToken)).ConfigureAwait(false); + AfterLoadCallback?.Invoke(_finalResults); + } + + protected abstract Task> GetResultsNonBatchedAsync(CancellationToken cancellationToken); + } +} diff --git a/src/NHibernate/Engine/ISessionImplementor.cs b/src/NHibernate/Engine/ISessionImplementor.cs index 772d7691249..530f146b261 100644 --- a/src/NHibernate/Engine/ISessionImplementor.cs +++ b/src/NHibernate/Engine/ISessionImplementor.cs @@ -10,14 +10,16 @@ using NHibernate.Hql; using NHibernate.Impl; using NHibernate.Loader.Custom; +using NHibernate.Multi; using NHibernate.Persister.Entity; using NHibernate.Transaction; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate.Engine { // 6.0 TODO: Convert to interface methods - internal static class SessionImplementorExtensions + internal static partial class SessionImplementorExtensions { internal static IDisposable BeginContext(this ISessionImplementor session) { @@ -35,6 +37,17 @@ internal static IDisposable BeginProcess(this ISessionImplementor session) // breaking change in case in custom session implementation is used. new SessionIdLoggingContext(session.SessionId); } + + //6.0 TODO: Expose as ISessionImplementor.FutureBatch and replace method usages with property + internal static IQueryBatch GetFutureBatch(this ISessionImplementor session) + { + return ReflectHelper.CastOrThrow(session, "future batch").FutureBatch; + } + + internal static void AutoFlushIfRequired(this ISessionImplementor implementor, ISet querySpaces) + { + (implementor as AbstractSessionImpl)?.AutoFlushIfRequired(querySpaces); + } } /// @@ -246,6 +259,8 @@ public partial interface ISessionImplementor IQuery GetNamedSQLQuery(string name); + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] IQueryTranslator[] GetQueries(IQueryExpression query, bool scalar); // NH specific for MultiQuery IInterceptor Interceptor { get; } @@ -326,8 +341,12 @@ public partial interface ISessionImplementor /// Execute a HQL update or delete query int ExecuteUpdate(IQueryExpression query, QueryParameters queryParameters); + //Since 5.2 + [Obsolete("Replaced by FutureBatch")] FutureCriteriaBatch FutureCriteriaBatch { get; } + //Since 5.2 + [Obsolete("Replaced by FutureBatch")] FutureQueryBatch FutureQueryBatch { get; } Guid SessionId { get; } diff --git a/src/NHibernate/ISession.cs b/src/NHibernate/ISession.cs index 71d09cb207c..fb0bf975010 100644 --- a/src/NHibernate/ISession.cs +++ b/src/NHibernate/ISession.cs @@ -7,8 +7,10 @@ using NHibernate.Event; using NHibernate.Event.Default; using NHibernate.Impl; +using NHibernate.Multi; using NHibernate.Stat; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate { @@ -26,6 +28,16 @@ public static ISharedStatelessSessionBuilder StatelessSessionWithOptions(this IS var impl = session as SessionImpl ?? throw new NotSupportedException("Only SessionImpl sessions are supported."); return impl.StatelessSessionWithOptions(); } + + /// + /// Creates a for the session. + /// + /// The session + /// A query batch. + public static IQueryBatch CreateQueryBatch(this ISession session) + { + return ReflectHelper.CastOrThrow(session, "query batch").CreateQueryBatch(); + } } /// diff --git a/src/NHibernate/IStatelessSession.cs b/src/NHibernate/IStatelessSession.cs index 495c8703b81..9fae5d78c72 100644 --- a/src/NHibernate/IStatelessSession.cs +++ b/src/NHibernate/IStatelessSession.cs @@ -4,9 +4,26 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Engine; +using NHibernate.Impl; +using NHibernate.Multi; +using NHibernate.Util; namespace NHibernate { + // 6.0 TODO: Convert to interface methods + public static class StatelessSessionExtensions + { + /// + /// Creates a for the session. + /// + /// The session + /// A query batch. + public static IQueryBatch CreateQueryBatch(this IStatelessSession session) + { + return ReflectHelper.CastOrThrow(session, "query batch").CreateQueryBatch(); + } + } + /// /// A command-oriented API for performing bulk operations against a database. /// diff --git a/src/NHibernate/Impl/AbstractQueryImpl.cs b/src/NHibernate/Impl/AbstractQueryImpl.cs index f77e4b4a1cf..3fa74af00f2 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl.cs @@ -4,11 +4,14 @@ using NHibernate.Engine; using NHibernate.Engine.Query; using NHibernate.Hql; +using NHibernate.Multi; using NHibernate.Proxy; using NHibernate.Transform; using NHibernate.Type; using NHibernate.Util; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Impl { @@ -903,14 +906,12 @@ public IQuery SetResultTransformer(IResultTransformer transformer) public IFutureEnumerable Future() { - session.FutureQueryBatch.Add(this); - return session.FutureQueryBatch.GetEnumerator(); + return session.GetFutureBatch().AddAsFuture(this); } public IFutureValue FutureValue() { - session.FutureQueryBatch.Add(this); - return session.FutureQueryBatch.GetFutureValue(); + return session.GetFutureBatch().AddAsFutureValue(this); } /// Override the current session cache mode, just for this query. @@ -1049,5 +1050,9 @@ public override string ToString() } protected internal abstract IEnumerable GetTranslators(ISessionImplementor sessionImplementor, QueryParameters queryParameters); + + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] + protected internal abstract Task> GetTranslatorsAsync(ISessionImplementor sessionImplementor, QueryParameters queryParameters, CancellationToken cancellationToken); } } diff --git a/src/NHibernate/Impl/AbstractQueryImpl2.cs b/src/NHibernate/Impl/AbstractQueryImpl2.cs index 5d60aab5bfc..b3c5c28be07 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl2.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl2.cs @@ -126,8 +126,8 @@ protected internal override IEnumerable GetTranslators(ISessionImpl // NOTE: updates queryParameters.NamedParameters as (desired) side effect var queryExpression = ExpandParameters(queryParameters.NamedParameters); - return sessionImplementor.GetQueries(queryExpression, false) - .Select(queryTranslator => new HqlTranslatorWrapper(queryTranslator)); + var plan = sessionImplementor.Factory.QueryPlanCache.GetHQLQueryPlan(queryExpression, false, sessionImplementor.EnabledFilters); + return plan.Translators.Select(t => new HqlTranslatorWrapper(t)); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Impl/AbstractSessionImpl.cs b/src/NHibernate/Impl/AbstractSessionImpl.cs index 64382f60c32..abf2879dda4 100644 --- a/src/NHibernate/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Impl/AbstractSessionImpl.cs @@ -16,6 +16,7 @@ using NHibernate.Linq; using NHibernate.Loader.Custom; using NHibernate.Loader.Custom.Sql; +using NHibernate.Multi; using NHibernate.Persister.Entity; using NHibernate.Transaction; using NHibernate.Type; @@ -30,6 +31,9 @@ public abstract partial class AbstractSessionImpl : ISessionImplementor private ISessionFactoryImplementor _factory; private FlushMode _flushMode; + [NonSerialized] + private IQueryBatch _futureMultiBatch; + private bool closed; /// Get the current NHibernate transaction. @@ -264,6 +268,8 @@ public virtual IQuery GetNamedSQLQuery(string name) /// public virtual DbConnection Connection => ConnectionManager.GetConnection(); + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public abstract IQueryTranslator[] GetQueries(IQueryExpression query, bool scalar); public abstract EventListeners Listeners { get; } public abstract bool IsEventSource { get; } @@ -275,8 +281,16 @@ public virtual IQuery GetNamedSQLQuery(string name) public abstract string BestGuessEntityName(object entity); public abstract string GuessEntityName(object entity); public abstract int ExecuteNativeUpdate(NativeSQLQuerySpecification specification, QueryParameters queryParameters); + + //Since 5.2 + [Obsolete("Replaced by FutureBatch")] public abstract FutureCriteriaBatch FutureCriteriaBatch { get; protected internal set; } + //Since 5.2 + [Obsolete("Replaced by FutureBatch")] public abstract FutureQueryBatch FutureQueryBatch { get; protected internal set; } + + public virtual IQueryBatch FutureBatch + =>_futureMultiBatch ?? (_futureMultiBatch = new QueryBatch(this, true)); public virtual IInterceptor Interceptor { get; protected set; } @@ -286,6 +300,18 @@ public virtual FlushMode FlushMode set => _flushMode = value; } + //6.0 TODO: Make abstract + /// + /// detect in-memory changes, determine if the changes are to tables + /// named in the query and, if so, complete execution the flush + /// + /// + /// Returns true if flush was executed + public virtual bool AutoFlushIfRequired(ISet querySpaces) + { + return false; + } + public virtual IQuery GetNamedQuery(string queryName) { using (BeginProcess()) @@ -615,5 +641,10 @@ public IQueryable Query(string entityName) { return new NhQueryable(this, entityName); } + + public virtual IQueryBatch CreateQueryBatch() + { + return new QueryBatch(this, false); + } } } diff --git a/src/NHibernate/Impl/CriteriaImpl.cs b/src/NHibernate/Impl/CriteriaImpl.cs index fb95f7e504f..5a6d4547aa7 100644 --- a/src/NHibernate/Impl/CriteriaImpl.cs +++ b/src/NHibernate/Impl/CriteriaImpl.cs @@ -4,6 +4,7 @@ using System.Text; using NHibernate.Criterion; using NHibernate.Engine; +using NHibernate.Multi; using NHibernate.SqlCommand; using NHibernate.Transform; using NHibernate.Util; @@ -457,14 +458,12 @@ public ICriteria CreateCriteria(string associationPath, string alias, JoinType j public IFutureValue FutureValue() { - session.FutureCriteriaBatch.Add(this); - return session.FutureCriteriaBatch.GetFutureValue(); + return session.GetFutureBatch().AddAsFutureValue(this); } public IFutureEnumerable Future() { - session.FutureCriteriaBatch.Add(this); - return session.FutureCriteriaBatch.GetEnumerator(); + return session.GetFutureBatch().AddAsFuture(this); } public object UniqueResult() diff --git a/src/NHibernate/Impl/DelayedEnumerator.cs b/src/NHibernate/Impl/DelayedEnumerator.cs index 5365a7db1f0..084d48ee2da 100644 --- a/src/NHibernate/Impl/DelayedEnumerator.cs +++ b/src/NHibernate/Impl/DelayedEnumerator.cs @@ -7,6 +7,8 @@ namespace NHibernate.Impl { + //Since 5.2 + [Obsolete] internal class DelayedEnumerator : IFutureEnumerable, IDelayedValue { public delegate IEnumerable GetResult(); @@ -73,6 +75,8 @@ public IList TransformList(IList collection) } } + //Since 5.2 + [Obsolete] internal interface IDelayedValue { Delegate ExecuteOnEval { get; set; } diff --git a/src/NHibernate/Impl/FutureBatch.cs b/src/NHibernate/Impl/FutureBatch.cs index 3d51a0ca17a..b94ed2180f9 100644 --- a/src/NHibernate/Impl/FutureBatch.cs +++ b/src/NHibernate/Impl/FutureBatch.cs @@ -6,6 +6,8 @@ namespace NHibernate.Impl { + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public abstract partial class FutureBatch { private class BatchedQuery diff --git a/src/NHibernate/Impl/FutureCriteriaBatch.cs b/src/NHibernate/Impl/FutureCriteriaBatch.cs index f8f0f889caf..1a530884943 100644 --- a/src/NHibernate/Impl/FutureCriteriaBatch.cs +++ b/src/NHibernate/Impl/FutureCriteriaBatch.cs @@ -1,7 +1,10 @@ +using System; using System.Collections; namespace NHibernate.Impl { + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public partial class FutureCriteriaBatch : FutureBatch { public FutureCriteriaBatch(SessionImpl session) : base(session) {} diff --git a/src/NHibernate/Impl/FutureQueryBatch.cs b/src/NHibernate/Impl/FutureQueryBatch.cs index ebf822e685b..3e391659b97 100644 --- a/src/NHibernate/Impl/FutureQueryBatch.cs +++ b/src/NHibernate/Impl/FutureQueryBatch.cs @@ -1,8 +1,11 @@ -using System.Collections; +using System; +using System.Collections; using NHibernate.Transform; namespace NHibernate.Impl { + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public partial class FutureQueryBatch : FutureBatch { public FutureQueryBatch(SessionImpl session) : base(session) diff --git a/src/NHibernate/Impl/FutureValue.cs b/src/NHibernate/Impl/FutureValue.cs index 65c93359e90..3788550ef50 100644 --- a/src/NHibernate/Impl/FutureValue.cs +++ b/src/NHibernate/Impl/FutureValue.cs @@ -7,6 +7,8 @@ namespace NHibernate.Impl { + //Since 5.2 + [Obsolete] internal class FutureValue : IFutureValue, IDelayedValue { public delegate IEnumerable GetResult(); diff --git a/src/NHibernate/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Impl/MultiCriteriaImpl.cs index 4e9947d0beb..021aeb05a0d 100644 --- a/src/NHibernate/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Impl/MultiCriteriaImpl.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using NHibernate.Cache; using NHibernate.Criterion; using NHibernate.Driver; @@ -77,9 +78,14 @@ public IList List() } } + var querySpaces = new HashSet(loaders.SelectMany(l => l.QuerySpaces)); + if (resultSetsCommand.HasQueries) + { + session.AutoFlushIfRequired(querySpaces); + } if (cacheable) { - criteriaResults = ListUsingQueryCache(); + criteriaResults = ListUsingQueryCache(querySpaces); } else { @@ -90,19 +96,17 @@ public IList List() } } - private IList ListUsingQueryCache() + private IList ListUsingQueryCache(HashSet querySpaces) { IQueryCache queryCache = session.Factory.GetQueryCache(cacheRegion); ISet filterKeys = FilterKey.CreateFilterKeys(session.EnabledFilters); - ISet querySpaces = new HashSet(); List resultTypesList = new List(); int[] maxRows = new int[loaders.Count]; int[] firstRows = new int[loaders.Count]; for (int i = 0; i < loaders.Count; i++) { - querySpaces.UnionWith(loaders[i].QuerySpaces); resultTypesList.Add(loaders[i].ResultTypes); firstRows[i] = parameters[i].RowSelection.FirstRow; maxRows[i] = parameters[i].RowSelection.MaxRows; diff --git a/src/NHibernate/Impl/MultiQueryImpl.cs b/src/NHibernate/Impl/MultiQueryImpl.cs index 2f28826b223..0fd9869b62e 100644 --- a/src/NHibernate/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Impl/MultiQueryImpl.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using NHibernate.Cache; using NHibernate.Driver; using NHibernate.Engine; @@ -423,7 +424,14 @@ public IList List() try { Before(); - return cacheable ? ListUsingQueryCache() : ListIgnoreQueryCache(); + + var querySpaces = new HashSet(Translators.SelectMany(t => t.QuerySpaces)); + if (resultSetsCommand.HasQueries) + { + session.AutoFlushIfRequired(querySpaces); + } + + return cacheable ? ListUsingQueryCache(querySpaces) : ListIgnoreQueryCache(); } finally { @@ -685,18 +693,16 @@ private IList ListIgnoreQueryCache() return GetResultList(DoList()); } - private IList ListUsingQueryCache() + private IList ListUsingQueryCache(HashSet querySpaces) { IQueryCache queryCache = session.Factory.GetQueryCache(cacheRegion); ISet filterKeys = FilterKey.CreateFilterKeys(session.EnabledFilters); - ISet querySpaces = new HashSet(); List resultTypesList = new List(Translators.Count); for (int i = 0; i < Translators.Count; i++) { ITranslator queryTranslator = Translators[i]; - querySpaces.UnionWith(queryTranslator.QuerySpaces); resultTypesList.Add(queryTranslator.ReturnTypes); } int[] firstRows = new int[Parameters.Count]; diff --git a/src/NHibernate/Impl/SessionImpl.cs b/src/NHibernate/Impl/SessionImpl.cs index cb86f7c0fd2..fd55e78f419 100644 --- a/src/NHibernate/Impl/SessionImpl.cs +++ b/src/NHibernate/Impl/SessionImpl.cs @@ -41,8 +41,12 @@ public sealed partial class SessionImpl : AbstractSessionImpl, IEventSource, ISe private CacheMode cacheMode = CacheMode.Normal; + //Since 5.2 + [Obsolete()] [NonSerialized] private FutureCriteriaBatch futureCriteriaBatch; + //Since 5.2 + [Obsolete()] [NonSerialized] private FutureQueryBatch futureQueryBatch; @@ -201,6 +205,8 @@ internal SessionImpl(SessionFactoryImpl factory, ISessionCreationOptions options } } + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public override FutureCriteriaBatch FutureCriteriaBatch { get @@ -215,6 +221,8 @@ protected internal set } } + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public override FutureQueryBatch FutureQueryBatch { get @@ -550,6 +558,8 @@ private void List(IQueryExpression queryExpression, QueryParameters queryParamet } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public override IQueryTranslator[] GetQueries(IQueryExpression query, bool scalar) { using (BeginProcess()) @@ -1026,8 +1036,8 @@ public override IPersistenceContext PersistenceContext /// named in the query and, if so, complete execution the flush /// /// - /// - private bool AutoFlushIfRequired(ISet querySpaces) + /// Returns true if flush was executed + public override bool AutoFlushIfRequired(ISet querySpaces) { using (BeginProcess()) { diff --git a/src/NHibernate/Impl/StatelessSessionImpl.cs b/src/NHibernate/Impl/StatelessSessionImpl.cs index d09e17aede4..7be2917a199 100644 --- a/src/NHibernate/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Impl/StatelessSessionImpl.cs @@ -278,6 +278,8 @@ public override IDictionary EnabledFilters get { return CollectionHelper.EmptyDictionary(); } } + // Since v5.2 + [Obsolete("This method has no usages and will be removed in a future version")] public override IQueryTranslator[] GetQueries(IQueryExpression query, bool scalar) { using (BeginContext()) @@ -853,12 +855,16 @@ public override int ExecuteUpdate(IQueryExpression queryExpression, QueryParamet } } + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public override FutureCriteriaBatch FutureCriteriaBatch { get { throw new NotSupportedException("future queries are not supported for stateless session"); } protected internal set { throw new NotSupportedException("future queries are not supported for stateless session"); } } + //Since 5.2 + [Obsolete("Replaced by QueryBatch")] public override FutureQueryBatch FutureQueryBatch { get { throw new NotSupportedException("future queries are not supported for stateless session"); } diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 4f478f9a61c..dc53e318cc5 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -10,18 +10,31 @@ using NHibernate.Type; using NHibernate.Util; using System.Threading.Tasks; +using NHibernate.Multi; namespace NHibernate.Linq { public partial interface INhQueryProvider : IQueryProvider { + //Since 5.2 + [Obsolete("Replaced by ISupportFutureBatchNhQueryProvider interface")] IFutureEnumerable ExecuteFuture(Expression expression); + + //Since 5.2 + [Obsolete("Replaced by ISupportFutureBatchNhQueryProvider interface")] IFutureValue ExecuteFutureValue(Expression expression); void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLinqExpression nhExpression, IDictionary> parameters); int ExecuteDml(QueryMode queryMode, Expression expression); Task ExecuteAsync(Expression expression, CancellationToken cancellationToken); } + // 6.0 TODO: merge into INhQueryProvider. + public interface ISupportFutureBatchNhQueryProvider + { + IQuery GetPreparedQuery(Expression expression, out NhLinqExpression nhExpression); + ISessionImplementor Session { get; } + } + /// /// The extended that supports setting options for underlying . /// @@ -35,7 +48,7 @@ public interface IQueryProviderWithOptions : IQueryProvider IQueryProvider WithOptions(Action setOptions); } - public partial class DefaultQueryProvider : INhQueryProvider, IQueryProviderWithOptions + public partial class DefaultQueryProvider : INhQueryProvider, IQueryProviderWithOptions, ISupportFutureBatchNhQueryProvider { private static readonly MethodInfo CreateQueryMethodDefinition = ReflectHelper.GetMethodDefinition((INhQueryProvider p) => p.CreateQuery(null)); @@ -65,7 +78,7 @@ private DefaultQueryProvider(ISessionImplementor session, object collection, NhQ public object Collection { get; } - protected virtual ISessionImplementor Session + public virtual ISessionImplementor Session { get { @@ -110,26 +123,30 @@ public virtual IQueryable CreateQuery(Expression expression) return new NhQueryable(this, expression); } + //Since 5.2 + [Obsolete("Replaced by ISupportFutureBatchNhQueryProvider interface")] public virtual IFutureEnumerable ExecuteFuture(Expression expression) { var nhExpression = PrepareQuery(expression, out var query); var result = query.Future(); - SetupFutureResult(nhExpression, (IDelayedValue)result); + if (result is IDelayedValue delayedValue) + SetupFutureResult(nhExpression, delayedValue); return result; } + //Since 5.2 + [Obsolete("Replaced by ISupportFutureBatchNhQueryProvider interface")] public virtual IFutureValue ExecuteFutureValue(Expression expression) { var nhExpression = PrepareQuery(expression, out var query); - - var result = query.FutureValue(); - SetupFutureResult(nhExpression, (IDelayedValue)result); - - return result; + var linqBatchItem = new LinqBatchItem(query, nhExpression); + return Session.GetFutureBatch().AddAsFutureValue(linqBatchItem); } + //Since 5.2 + [Obsolete] private static void SetupFutureResult(NhLinqExpression nhExpression, IDelayedValue result) { if (nhExpression.ExpressionToHqlTranslationResults.PostExecuteTransformer == null) @@ -273,5 +290,11 @@ public int ExecuteDml(QueryMode queryMode, Expression expression) _options?.Apply(query); return query.ExecuteUpdate(); } + + public IQuery GetPreparedQuery(Expression expression, out NhLinqExpression nhExpression) + { + nhExpression = PrepareQuery(expression, out var query); + return query; + } } } diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index f6f225b1c12..18473d80128 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Impl; +using NHibernate.Multi; using NHibernate.Type; using NHibernate.Util; using Remotion.Linq.Parsing.ExpressionVisitors; using System.Threading; using System.Threading.Tasks; +using NHibernate.Engine; namespace NHibernate.Linq { @@ -2404,8 +2406,14 @@ async Task> InternalToListAsync() /// is not a . public static IFutureEnumerable ToFuture(this IQueryable source) { + if (source.Provider is ISupportFutureBatchNhQueryProvider batchProvider) + { + return batchProvider.Session.GetFutureBatch().AddAsFuture(source); + } +#pragma warning disable CS0618 // Type or member is obsolete var provider = GetNhProvider(source); return provider.ExecuteFuture(source.Expression); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -2419,9 +2427,15 @@ public static IFutureEnumerable ToFuture(this IQueryable is not a . public static IFutureValue ToFutureValue(this IQueryable source) { + if (source.Provider is ISupportFutureBatchNhQueryProvider batchProvider) + { + return batchProvider.Session.GetFutureBatch().AddAsFutureValue(source); + } +#pragma warning disable CS0618, CS0612// Type or member is obsolete var provider = GetNhProvider(source); var future = provider.ExecuteFuture(source.Expression); return new FutureValue(future.GetEnumerable, future.GetEnumerableAsync); +#pragma warning restore CS0618, CS0612// Type or member is obsolete } /// @@ -2437,12 +2451,16 @@ public static IFutureValue ToFutureValue(this IQueryable is not a . public static IFutureValue ToFutureValue(this IQueryable source, Expression, TResult>> selector) { - var provider = GetNhProvider(source); - + if (source.Provider is ISupportFutureBatchNhQueryProvider batchProvider) + { + return batchProvider.Session.GetFutureBatch().AddAsFutureValue(source, selector); + } +#pragma warning disable CS0618 // Type or member is obsolete var expression = ReplacingExpressionVisitor .Replace(selector.Parameters.Single(), source.Expression, selector.Body); - + var provider = GetNhProvider(source); return provider.ExecuteFutureValue(expression); +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 719e11db8a4..598bcb39c59 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1637,7 +1637,7 @@ protected IList List(ISessionImplementor session, QueryParameters queryParameter /// protected IList List(ISessionImplementor session, QueryParameters queryParameters, ISet querySpaces) { - bool cacheable = _factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable; + var cacheable = IsCacheable(queryParameters); if (cacheable) { @@ -1646,6 +1646,11 @@ protected IList List(ISessionImplementor session, QueryParameters queryParameter return ListIgnoreQueryCache(session, queryParameters); } + internal bool IsCacheable(QueryParameters queryParameters) + { + return _factory.Settings.IsQueryCacheEnabled && queryParameters.Cacheable; + } + private IList ListIgnoreQueryCache(ISessionImplementor session, QueryParameters queryParameters) { return GetResultList(DoList(session, queryParameters), queryParameters.ResultTransformer); @@ -1665,23 +1670,28 @@ private IList ListUsingQueryCache(ISessionImplementor session, QueryParameters q PutResultInQueryCache(session, queryParameters, queryCache, key, result); } - IResultTransformer resolvedTransformer = ResolveResultTransformer(queryParameters.ResultTransformer); - if (resolvedTransformer != null) - { - result = (AreResultSetRowsTransformedImmediately() - ? key.ResultTransformer.RetransformResults( - result, - ResultRowAliases, - queryParameters.ResultTransformer, - IncludeInResultRow) - : key.ResultTransformer.UntransformToTuples(result) - ); - } + result = TransformCacheableResults(queryParameters, key.ResultTransformer, result); return GetResultList(result, queryParameters.ResultTransformer); } - private QueryKey GenerateQueryKey(ISessionImplementor session, QueryParameters queryParameters) + internal IList TransformCacheableResults(QueryParameters queryParameters, CacheableResultTransformer transformer, IList result) + { + var resolvedTransformer = ResolveResultTransformer(queryParameters.ResultTransformer); + if (resolvedTransformer == null) + return result; + + return (AreResultSetRowsTransformedImmediately() + ? transformer.RetransformResults( + result, + ResultRowAliases, + queryParameters.ResultTransformer, + IncludeInResultRow) + : transformer.UntransformToTuples(result) + ); + } + + internal QueryKey GenerateQueryKey(ISessionImplementor session, QueryParameters queryParameters) { ISet filterKeys = FilterKey.CreateFilterKeys(session.EnabledFilters); return new QueryKey(Factory, SqlString, queryParameters, filterKeys, @@ -1695,7 +1705,7 @@ private CacheableResultTransformer CreateCacheableResultTransformer(QueryParamet queryParameters.HasAutoDiscoverScalarTypes, SqlString); } - private IList GetResultFromQueryCache(ISessionImplementor session, QueryParameters queryParameters, + internal IList GetResultFromQueryCache(ISessionImplementor session, QueryParameters queryParameters, ISet querySpaces, IQueryCache queryCache, QueryKey key) { @@ -1739,7 +1749,7 @@ private IList GetResultFromQueryCache(ISessionImplementor session, QueryParamete return result; } - private void PutResultInQueryCache(ISessionImplementor session, QueryParameters queryParameters, + internal void PutResultInQueryCache(ISessionImplementor session, QueryParameters queryParameters, IQueryCache queryCache, QueryKey key, IList result) { if (session.CacheMode.HasFlag(CacheMode.Put)) diff --git a/src/NHibernate/Multi/CriteriaBatchItem.cs b/src/NHibernate/Multi/CriteriaBatchItem.cs new file mode 100644 index 00000000000..306ffa59454 --- /dev/null +++ b/src/NHibernate/Multi/CriteriaBatchItem.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using NHibernate.Impl; +using NHibernate.Loader.Criteria; +using NHibernate.Persister.Entity; + +namespace NHibernate.Multi +{ + public partial class CriteriaBatchItem : QueryBatchItemBase + { + private readonly CriteriaImpl _criteria; + + public CriteriaBatchItem(ICriteria query) + { + _criteria = (CriteriaImpl) query ?? throw new ArgumentNullException(nameof(query)); + } + + protected override List GetQueryLoadInfo() + { + var factory = Session.Factory; + //for detached criteria + if (_criteria.Session == null) + _criteria.Session = Session; + + string[] implementors = factory.GetImplementors(_criteria.EntityOrClassName); + int size = implementors.Length; + var list = new List(size); + for (int i = 0; i < size; i++) + { + CriteriaLoader loader = new CriteriaLoader( + factory.GetEntityPersister(implementors[i]) as IOuterJoinLoadable, + factory, + _criteria, + implementors[i], + Session.EnabledFilters + ); + + list.Add( + new QueryLoadInfo() + { + Loader = loader, + Parameters = loader.Translator.GetQueryParameters(), + QuerySpaces = loader.QuerySpaces, + }); + } + + return list; + } + + protected override IList GetResultsNonBatched() + { + return _criteria.List(); + } + + protected override List DoGetResults() + { + return GetTypedResults(); + } + } +} diff --git a/src/NHibernate/Multi/IFutureList.cs b/src/NHibernate/Multi/IFutureList.cs new file mode 100644 index 00000000000..29b4220abf9 --- /dev/null +++ b/src/NHibernate/Multi/IFutureList.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace NHibernate.Multi +{ + internal interface IFutureList : IFutureValue> + { } +} diff --git a/src/NHibernate/Multi/IQueryBatch.cs b/src/NHibernate/Multi/IQueryBatch.cs new file mode 100644 index 00000000000..da3beccbb40 --- /dev/null +++ b/src/NHibernate/Multi/IQueryBatch.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Multi +{ + /// + /// Universal query batcher + /// + public partial interface IQueryBatch + { + /// + /// Executes the batch. + /// + void Execute(); + + /// + /// Returns true if batch is already executed or empty + /// + bool IsExecutedOrEmpty { get; } + + /// + /// Adds a query to the batch. + /// + /// The query. + /// Thrown if the batch has already been executed. + /// Thrown if is . + void Add(IQueryBatchItem query); + + /// + /// Adds a query to the batch. + /// + /// A key for retrieval of the query result. + /// The query. + /// Thrown if the batch has already been executed. + /// Thrown if is . + void Add(string key, IQueryBatchItem query); + + /// + /// Gets a query result, triggering execution of the batch if it was not already executed. + /// + /// The index of the query for which results are to be obtained. + /// The type of the result elements of the query. + /// A query result. + /// is 0 based and matches the order in which queries have been + /// added into the batch. + IList GetResult(int queryIndex); + + /// + /// Gets a query result, triggering execution of the batch if it was not already executed. + /// + /// The key of the query for which results are to be obtained. + /// The type of the result elements of the query. + /// A query result. + IList GetResult(string querykey); + + /// + /// The timeout in seconds for the underlying ADO.NET query. + /// + int? Timeout { get; set; } + + /// + /// The session flush mode to use during the batch execution. + /// + FlushMode? FlushMode { get; set; } + } +} diff --git a/src/NHibernate/Multi/IQueryBatchItem.cs b/src/NHibernate/Multi/IQueryBatchItem.cs new file mode 100644 index 00000000000..e892e862b17 --- /dev/null +++ b/src/NHibernate/Multi/IQueryBatchItem.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using NHibernate.Engine; +using NHibernate.SqlCommand; + +namespace NHibernate.Multi +{ + /// + /// Interface for wrapping query to be batched by + /// + public interface IQueryBatchItem : IQueryBatchItem + { + /// + /// Returns loaded typed results by query. + /// Must be called only after . + /// + IList GetResults(); + + /// + /// Callback is executed after results are loaded by batch. + /// Loaded results are provided in action parameter. + /// + Action> AfterLoadCallback { get; set; } + } + + /// + /// Interface for wrapping query to be batched by + /// + public partial interface IQueryBatchItem + { + /// + /// Method is called right before batch execution. + /// Can be used for various delayed initialization logic. + /// + /// + void Init(ISessionImplementor session); + + /// + /// Returns commands generated by query + /// + IEnumerable GetCommands(); + + /// + /// Returns delegates for processing result sets generated by . + /// Delegate should return number of rows loaded by command. + /// + /// + IEnumerable> GetResultSetHandler(); + + /// + /// Executed after all commands in batch are processed + /// + void ProcessResults(); + + /// + /// Immediate query execution in case the dialect does not support batches + /// + void ExecuteNonBatched(); + + /// + /// Get cache query spaces + /// + IEnumerable GetQuerySpaces(); + } +} diff --git a/src/NHibernate/Multi/LinqBatchItem.cs b/src/NHibernate/Multi/LinqBatchItem.cs new file mode 100644 index 00000000000..fdbf9f1be2f --- /dev/null +++ b/src/NHibernate/Multi/LinqBatchItem.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Linq; +using NHibernate.Util; +using Remotion.Linq.Parsing.ExpressionVisitors; + +namespace NHibernate.Multi +{ + public static class LinqBatchItem + { + public static LinqBatchItem Create(IQueryable query, Expression, TResult>> selector) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + if (selector == null) + throw new ArgumentNullException(nameof(selector)); + var expression = ReplacingExpressionVisitor + .Replace(selector.Parameters.Single(), query.Expression, selector.Body); + return GetForQuery(query, expression); + } + + public static LinqBatchItem Create(IQueryable query) + { + return GetForQuery(query, null); + } + + private static LinqBatchItem GetForQuery(IQueryable query, Expression ex = null) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + var prov = (ISupportFutureBatchNhQueryProvider) query.Provider; + + var q = prov.GetPreparedQuery(ex ?? query.Expression, out var linqEx); + return new LinqBatchItem(q, linqEx); + } + } + + /// + /// Create instance via methods + /// + /// Result type + public partial class LinqBatchItem : QueryBatchItem + { + private readonly Delegate _postExecuteTransformer; + + public LinqBatchItem(IQuery query) : base(query) + { + } + + internal LinqBatchItem(IQuery query, NhLinqExpression linq) : base(query) + { + _postExecuteTransformer = linq.ExpressionToHqlTranslationResults.PostExecuteTransformer; + } + + protected override IList GetResultsNonBatched() + { + if (_postExecuteTransformer == null) + { + return base.GetResultsNonBatched(); + } + + return GetTransformedResults(Query.List()); + } + + protected override List DoGetResults() + { + if (_postExecuteTransformer != null) + { + var elementType = GetResultTypeIfChanged(); + + IList transformerList = elementType == null + ? base.DoGetResults() + : GetTypedResults(elementType); + + return GetTransformedResults(transformerList); + } + + return base.DoGetResults(); + } + + private List GetTransformedResults(IList transformerList) + { + var res = _postExecuteTransformer.DynamicInvoke(transformerList.AsQueryable()); + return new List + { + (T) res + }; + } + + private System.Type GetResultTypeIfChanged() + { + if (_postExecuteTransformer == null) + { + return null; + } + var elementType = _postExecuteTransformer.Method.GetParameters()[1].ParameterType.GetGenericArguments()[0]; + if (typeof(T).IsAssignableFrom(elementType)) + { + return null; + } + + return elementType; + } + + private IList GetTypedResults(System.Type type) + { + var method = ReflectHelper.GetMethod(() => GetTypedResults()) + .GetGenericMethodDefinition(); + var generic = method.MakeGenericMethod(type); + return (IList) generic.Invoke(this, null); + } + } +} diff --git a/src/NHibernate/Multi/QueryBatch.cs b/src/NHibernate/Multi/QueryBatch.cs new file mode 100644 index 00000000000..7e92c6419bf --- /dev/null +++ b/src/NHibernate/Multi/QueryBatch.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NHibernate.Driver; +using NHibernate.Engine; +using NHibernate.Exceptions; + +namespace NHibernate.Multi +{ + /// + public partial class QueryBatch : IQueryBatch + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(QueryBatch)); + + private readonly bool _autoReset; + private readonly List _queries = new List(); + private readonly Dictionary _queriesByKey = new Dictionary(); + private bool _executed; + + public QueryBatch(ISessionImplementor session, bool autoReset) + { + Session = session; + _autoReset = autoReset; + } + + protected ISessionImplementor Session { get; } + + /// + public int? Timeout { get; set; } + + /// + public FlushMode? FlushMode { get; set; } + + /// + public void Execute() + { + if (_queries.Count == 0) + return; + var sessionFlushMode = Session.FlushMode; + if (FlushMode.HasValue) + Session.FlushMode = FlushMode.Value; + try + { + Init(); + + if (!Session.Factory.ConnectionProvider.Driver.SupportsMultipleQueries) + { + foreach (var query in _queries) + { + query.ExecuteNonBatched(); + } + return; + } + + using (Session.BeginProcess()) + { + DoExecute(); + } + } + finally + { + if (_autoReset) + { + _queries.Clear(); + _queriesByKey.Clear(); + } + else + _executed = true; + + if (FlushMode.HasValue) + Session.FlushMode = sessionFlushMode; + } + } + + /// + public bool IsExecutedOrEmpty => _executed || _queries.Count == 0; + + /// + public void Add(IQueryBatchItem query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + if (_executed) + throw new InvalidOperationException("The batch has already been executed, use another batch"); + _queries.Add(query); + } + + /// + public void Add(string key, IQueryBatchItem query) + { + Add(query); + _queriesByKey.Add(key, query); + } + + /// + public IList GetResult(int queryIndex) + { + return GetResults(_queries[queryIndex]); + } + + /// + public IList GetResult(string querykey) + { + return GetResults(_queriesByKey[querykey]); + } + + private IList GetResults(IQueryBatchItem query) + { + if (!_executed) + Execute(); + return ((IQueryBatchItem)query).GetResults(); + } + + private void Init() + { + foreach (var query in _queries) + { + query.Init(Session); + } + } + + private void CombineQueries(IResultSetsCommand resultSetsCommand) + { + foreach (var multiSource in _queries) + foreach (var cmd in multiSource.GetCommands()) + { + resultSetsCommand.Append(cmd); + } + } + + protected void DoExecute() + { + var resultSetsCommand = Session.Factory.ConnectionProvider.Driver.GetResultSetsCommand(Session); + CombineQueries(resultSetsCommand); + + var querySpaces = new HashSet(_queries.SelectMany(t => t.GetQuerySpaces())); + if (resultSetsCommand.HasQueries) + { + Session.AutoFlushIfRequired(querySpaces); + } + + bool statsEnabled = Session.Factory.Statistics.IsStatisticsEnabled; + Stopwatch stopWatch = null; + if (statsEnabled) + { + stopWatch = new Stopwatch(); + stopWatch.Start(); + } + if (Log.IsDebugEnabled()) + { + Log.Debug("Multi query with {0} queries: {1}", _queries.Count, resultSetsCommand.Sql); + } + + int rowCount = 0; + try + { + if (resultSetsCommand.HasQueries) + { + using (var reader = resultSetsCommand.GetReader(Timeout)) + { + foreach (var multiSource in _queries) + { + foreach (var resultSetHandler in multiSource.GetResultSetHandler()) + { + rowCount += resultSetHandler(reader); + reader.NextResult(); + } + } + } + } + + foreach (var multiSource in _queries) + { + multiSource.ProcessResults(); + } + } + catch (Exception sqle) + { + Log.Error(sqle, "Failed to execute multi query: [{0}]", resultSetsCommand.Sql); + throw ADOExceptionHelper.Convert(Session.Factory.SQLExceptionConverter, sqle, "Failed to execute multi query", resultSetsCommand.Sql); + } + + if (statsEnabled) + { + stopWatch.Stop(); + Session.Factory.StatisticsImplementor.QueryExecuted($"{_queries.Count} queries", rowCount, stopWatch.Elapsed); + } + } + } +} diff --git a/src/NHibernate/Multi/QueryBatchExtensions.cs b/src/NHibernate/Multi/QueryBatchExtensions.cs new file mode 100644 index 00000000000..c5405ff9a66 --- /dev/null +++ b/src/NHibernate/Multi/QueryBatchExtensions.cs @@ -0,0 +1,607 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using NHibernate.Criterion; +using NHibernate.Engine; + +namespace NHibernate.Multi +{ + public static partial class QueryBatchExtensions + { + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, IQueryOver query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, IQueryOver query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, IQueryOver query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, IQueryOver query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, ICriteria query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, ICriteria query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, DetachedCriteria query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, DetachedCriteria query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, IQuery query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, IQuery query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, IQueryable query, Action> afterLoad = null) + { + return batch.Add(For(query), afterLoad); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// The type of the query result elements. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, IQueryable query) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query)); + return batch; + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// The query. + /// An aggregation function to apply to . + /// Callback to execute when query is loaded. Loaded results are provided as action parameter. + /// The type of the query elements before aggregation. + /// The type resulting of the query result aggregation. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, IQueryable query, Expression, TResult>> selector, Action afterLoad = null) + { + return batch.Add(For(query, selector), afterLoad == null ? (Action>) null : list => afterLoad(list.FirstOrDefault())); + } + + /// + /// Adds a query to the batch. + /// + /// The batch. + /// A key for retrieval of the query result. + /// The query. + /// An aggregation function to apply to . + /// The type of the query elements before aggregation. + /// The type resulting of the query result aggregation. + /// Thrown if the batch has already been executed. + /// Thrown if is . + /// The batch instance for method chain. + public static IQueryBatch Add(this IQueryBatch batch, string key, IQueryable query, Expression, TResult>> selector) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Add(key, For(query, selector)); + return batch; + } + + /// + /// Sets the timeout in seconds for the underlying ADO.NET query. + /// + /// The batch. + /// The timeout for the batch. + /// The batch instance for method chain. + public static IQueryBatch SetTimeout(this IQueryBatch batch, int? timeout) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.Timeout = timeout == RowSelection.NoValue ? null : timeout; + return batch; + } + + /// + /// Overrides the current session flush mode, just for this query batch. + /// + /// The batch. + /// The flush mode for the batch. + /// The batch instance for method chain. + public static IQueryBatch SetFlushMode(this IQueryBatch batch, FlushMode mode) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + batch.FlushMode = mode; + return batch; + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, IQueryOver query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, IQueryOver query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, ICriteria query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, DetachedCriteria query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, IQuery query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, IQueryable query) + { + return AddAsFuture(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureEnumerable AddAsFuture(this IQueryBatch batch, IQueryBatchItem query) + { + batch.Add(query); + return new FutureEnumerable(batch, query); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// An aggregation function to apply to . + /// The type of the query elements before aggregation. + /// The type resulting of the query result aggregation. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQueryable query, Expression, TResult>> selector) + { + return AddAsFutureValue(batch, For(query, selector)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQueryable query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, ICriteria query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, DetachedCriteria query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQueryOver query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQueryOver query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQuery query) + { + return AddAsFutureValue(batch, For(query)); + } + + /// + /// Adds a query to the batch, returning it as an . + /// + /// The batch. + /// The query. + /// The type of the query result elements. + /// A future query which execution will be handled by the batch. + public static IFutureValue AddAsFutureValue(this IQueryBatch batch, IQueryBatchItem query) + { + batch.Add(query); + return new FutureValue(batch, query); + } + + private static LinqBatchItem For(IQueryable query) + { + return LinqBatchItem.Create(query); + } + + private static LinqBatchItem For(IQueryable query, Expression, TResult>> selector) + { + return LinqBatchItem.Create(query, selector); + } + + private static QueryBatchItem For(IQuery query) + { + return new QueryBatchItem(query); + } + + private static CriteriaBatchItem For(ICriteria query) + { + return new CriteriaBatchItem(query); + } + + private static CriteriaBatchItem For(DetachedCriteria query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + return new CriteriaBatchItem(query.GetCriteriaImpl()); + } + + private static CriteriaBatchItem For(IQueryOver query) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + return For(query.RootCriteria); + } + + private static IQueryBatch Add(this IQueryBatch batch, IQueryBatchItem query, Action> afterLoad) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + if (query == null) + throw new ArgumentNullException(nameof(query)); + if (afterLoad != null) + { + query.AfterLoadCallback += afterLoad; + } + batch.Add(query); + return batch; + } + + #region Helper classes + + partial class FutureValue : IFutureValue + { + private FutureList _futureList; + + private TResult _result; + + public FutureValue(IQueryBatch batch, IQueryBatchItem query) + { + _futureList = new FutureList(batch, query); + } + + public TResult Value + { + get + { + if (_futureList == null) + return _result; + + _result = _futureList.Value.FirstOrDefault(); + _futureList = null; + + return _result; + } + } + } + + partial class FutureList : IFutureList + { + private IQueryBatch _batch; + private IQueryBatchItem _query; + + private IList _list; + + public FutureList(IQueryBatch batch, IQueryBatchItem query) + { + _batch = batch; + _query = query; + } + + public IList Value + { + get + { + if (_batch == null) + return _list; + + if (!_batch.IsExecutedOrEmpty) + _batch.Execute(); + _list = _query.GetResults(); + + _batch = null; + _query = null; + + return _list; + } + } + } + + class FutureEnumerable : IFutureEnumerable + { + private readonly IFutureList _result; + + public FutureEnumerable(IQueryBatch batch, IQueryBatchItem query) + { + _result = new FutureList(batch, query); + } + + public async Task> GetEnumerableAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return await _result.GetValueAsync(cancellationToken); + } + + public IEnumerable GetEnumerable() + { + return _result.Value; + } + + IEnumerator IFutureEnumerable.GetEnumerator() + { + return GetEnumerable().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerable().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerable().GetEnumerator(); + } + } + + #endregion Helper classes + } +} diff --git a/src/NHibernate/Multi/QueryBatchItem.cs b/src/NHibernate/Multi/QueryBatchItem.cs new file mode 100644 index 00000000000..5a2569199f4 --- /dev/null +++ b/src/NHibernate/Multi/QueryBatchItem.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Impl; + +namespace NHibernate.Multi +{ + public partial class QueryBatchItem : QueryBatchItemBase + { + protected readonly AbstractQueryImpl Query; + + public QueryBatchItem(IQuery query) + { + Query = (AbstractQueryImpl) query ?? throw new ArgumentNullException(nameof(query)); + } + + protected override List GetQueryLoadInfo() + { + 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(); + } + + protected override IList GetResultsNonBatched() + { + return Query.List(); + } + + protected override List DoGetResults() + { + return GetTypedResults(); + } + } +} diff --git a/src/NHibernate/Multi/QueryBatchItemBase.cs b/src/NHibernate/Multi/QueryBatchItemBase.cs new file mode 100644 index 00000000000..4fc5535b230 --- /dev/null +++ b/src/NHibernate/Multi/QueryBatchItemBase.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Engine; +using NHibernate.SqlCommand; +using NHibernate.Util; + +namespace NHibernate.Multi +{ + /// + /// Base class for both ICriteria and IQuery queries + /// + public abstract partial class QueryBatchItemBase : IQueryBatchItem + { + protected ISessionImplementor Session; + private List[] _subselectResultKeys; + private IList[] _loaderResults; + + private List _queryInfos; + private IList _finalResults; + + protected class QueryLoadInfo + { + public Loader.Loader Loader; + public QueryParameters Parameters; + + //Cache related properties: + public ISet QuerySpaces; + public IQueryCache Cache; + public QueryKey CacheKey; + } + + protected abstract List GetQueryLoadInfo(); + + public virtual void Init(ISessionImplementor session) + { + Session = session; + + _queryInfos = GetQueryLoadInfo(); + + var count = _queryInfos.Count; + _subselectResultKeys = new List[count]; + _loaderResults = new IList[count]; + + _finalResults = null; + } + + /// + /// Gets the commands to execute for getting the not-already cached results of this query. Does retrieves + /// already cached results by side-effect. + /// + /// The commands for obtaining the results not already cached. + public IEnumerable GetCommands() + { + for (var index = 0; index < _queryInfos.Count; index++) + { + var qi = _queryInfos[index]; + + if (qi.Loader.IsCacheable(qi.Parameters)) + { + // 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); + + if (resultsFromCache != null) + { + // Cached results available, skip the command for them and stores them. + qi.Cache = null; + _loaderResults[index] = resultsFromCache; + continue; + } + } + + yield return qi.Loader.CreateSqlCommand(qi.Parameters, Session); + } + } + + public IEnumerable> GetResultSetHandler() + { + var dialect = Session.Factory.Dialect; + List[] hydratedObjects = new List[_queryInfos.Count]; + + for (var i = 0; i < _queryInfos.Count; i++) + { + Loader.Loader loader = _queryInfos[i].Loader; + var queryParameters = _queryInfos[i].Parameters; + + //Skip processing for items already loaded from cache + if (_queryInfos[i].CacheKey?.ResultTransformer != null && _loaderResults[i] != null) + { + _loaderResults[i] = loader.TransformCacheableResults(queryParameters, _queryInfos[i].CacheKey.ResultTransformer, _loaderResults[i]); + continue; + } + + int entitySpan = loader.EntityPersisters.Length; + hydratedObjects[i] = entitySpan == 0 ? null : new List(entitySpan); + EntityKey[] keys = new EntityKey[entitySpan]; + + RowSelection selection = queryParameters.RowSelection; + bool createSubselects = loader.IsSubselectLoadingEnabled; + + _subselectResultKeys[i] = createSubselects ? new List() : null; + int maxRows = Loader.Loader.HasMaxRows(selection) ? selection.MaxRows : int.MaxValue; + bool advanceSelection = !dialect.SupportsLimitOffset || !loader.UseLimit(selection, dialect); + + var index = i; + yield return reader => + { + if (advanceSelection) + { + Loader.Loader.Advance(reader, selection); + } + if (queryParameters.HasAutoDiscoverScalarTypes) + { + loader.AutoDiscoverTypes(reader, queryParameters, null); + } + + LockMode[] lockModeArray = loader.GetLockModes(queryParameters.LockModes); + EntityKey optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); + int rowCount = 0; + var tmpResults = new List(); + + int count; + for (count = 0; count < maxRows && reader.Read(); count++) + { + rowCount++; + + object o = + loader.GetRowFromResultSet( + reader, + Session, + queryParameters, + lockModeArray, + optionalObjectKey, + hydratedObjects[index], + keys, + true, + _queryInfos[index].CacheKey?.ResultTransformer + ); + if (loader.IsSubselectLoadingEnabled) + { + _subselectResultKeys[index].Add(keys); + keys = new EntityKey[entitySpan]; //can't reuse in this case + } + + tmpResults.Add(o); + } + _loaderResults[index] = tmpResults; + + if (index == _queryInfos.Count - 1) + { + InitializeEntitiesAndCollections(reader, hydratedObjects); + } + return rowCount; + }; + } + } + + public void ProcessResults() + { + for (int 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.Cache != null) + { + queryInfo.Loader.PutResultInQueryCache(Session, queryInfo.Parameters, queryInfo.Cache, queryInfo.CacheKey, _loaderResults[i]); + } + } + AfterLoadCallback?.Invoke(GetResults()); + } + + public void ExecuteNonBatched() + { + _finalResults = GetResultsNonBatched(); + AfterLoadCallback?.Invoke(_finalResults); + } + + public IEnumerable GetQuerySpaces() + { + return _queryInfos.SelectMany(q => q.QuerySpaces); + } + + protected abstract IList GetResultsNonBatched(); + + protected List GetTypedResults() + { + if (_loaderResults == null) + { + throw new HibernateException("Batch wasn't executed. You must call IQueryBatch.Execute() before accessing results."); + } + List results = new List(_loaderResults.Sum(tr => tr.Count)); + for (int i = 0; i < _queryInfos.Count; i++) + { + var list = _queryInfos[i].Loader.GetResultList( + _loaderResults[i], + _queryInfos[i].Parameters.ResultTransformer); + ArrayHelper.AddAll(results, list); + } + + return results; + } + + public IList GetResults() + { + return _finalResults ?? (_finalResults = DoGetResults()); + } + + public Action> AfterLoadCallback { get; set; } + + protected abstract List DoGetResults(); + + private void InitializeEntitiesAndCollections(DbDataReader reader, List[] hydratedObjects) + { + for (int i = 0; i < _queryInfos.Count; i++) + { + _queryInfos[i].Loader.InitializeEntitiesAndCollections( + hydratedObjects[i], reader, Session, Session.PersistenceContext.DefaultReadOnly); + } + } + } +} diff --git a/src/NHibernate/Util/ReflectHelper.cs b/src/NHibernate/Util/ReflectHelper.cs index 017cdd3a197..0c90dc29761 100644 --- a/src/NHibernate/Util/ReflectHelper.cs +++ b/src/NHibernate/Util/ReflectHelper.cs @@ -29,8 +29,12 @@ public static class ReflectHelper internal static T CastOrThrow(object obj, string supportMessage) where T : class { - return obj as T - ?? throw new ArgumentException($"{obj?.GetType().FullName} requires to implement {typeof(T).FullName} interface to support {supportMessage}."); + if (obj is T t) + return t; + + var typeKind = typeof(T).IsInterface ? "interface" : "class"; + var objType = obj?.GetType().FullName ?? "Object must not be null and"; + throw new ArgumentException($@"{objType} requires to implement {typeof(T).FullName} {typeKind} to support {supportMessage}."); } ///