diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 7b8a269d01c..3c541dc7315 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -170,6 +170,8 @@ applyChanges: true analyzation: methodConversion: + - conversion: Copy + name: AfterTransactionCompletionProcess_EvictsFromCache - conversion: Copy hasAttributeName: OneTimeSetUpAttribute - conversion: Copy diff --git a/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs b/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs new file mode 100644 index 00000000000..4361dbefa5a --- /dev/null +++ b/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// 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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NSubstitute; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.BulkManipulation +{ + using System.Threading.Tasks; + [TestFixture] + public class NativeSQLBulkOperationsWithCacheAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "BulkManipulation.Vehicle.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.UseQueryCache, "true"); + cfg.SetProperty(Environment.UseSecondLevelCache, "true"); + cfg.SetProperty(Environment.CacheProvider, typeof(SubstituteCacheProvider).AssemblyQualifiedName); + } + + [Test] + public async Task SimpleNativeSQLInsert_DoesNotEvictEntireCacheWhenQuerySpacesAreAddedAsync() + { + List clearCalls = new List(); + (Sfi.Settings.CacheProvider as SubstituteCacheProvider).OnClear(x => + { + clearCalls.Add(x); + }); + using (var s = OpenSession()) + { + string ssql = "UPDATE Vehicle SET Vin='123' WHERE Vin='123c'"; + + using (var t = s.BeginTransaction()) + { + + await (s.CreateSQLQuery(ssql).ExecuteUpdateAsync()); + await (t.CommitAsync()); + + Assert.AreEqual(1, clearCalls.Count); + } + + clearCalls.Clear(); + + using (var t = s.BeginTransaction()) + { + await (s.CreateSQLQuery(ssql).AddSynchronizedQuerySpace("Unknown").ExecuteUpdateAsync()); + await (t.CommitAsync()); + + Assert.AreEqual(0, clearCalls.Count); + } + } + } + } +} diff --git a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs index 3a69352f4f8..4b3d76497da 100644 --- a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -54,7 +54,7 @@ public class GeneralTestAsync : TestCase protected override IList Mappings { - get { return new[] { "SqlTest.Query.NativeSQLQueries.hbm.xml" }; } + get { return new[] {"SqlTest.Query.NativeSQLQueries.hbm.xml"}; } } protected override string MappingsAssembly @@ -103,17 +103,17 @@ public async Task SQLQueryInterfaceAsync() await (s.SaveAsync(emp)); IList l = await (s.CreateSQLQuery(OrgEmpRegionSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddScalar("regionCode", NHibernateUtil.String) - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddScalar("regionCode", NHibernateUtil.String) + .ListAsync()); Assert.AreEqual(2, l.Count); l = await (s.CreateSQLQuery(OrgEmpPersonSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddJoin("pers", "emp.employee") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddJoin("pers", "emp.employee") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (t.CommitAsync()); @@ -122,13 +122,14 @@ public async Task SQLQueryInterfaceAsync() s = OpenSession(); t = s.BeginTransaction(); - l = await (s.CreateSQLQuery("select {org.*}, {emp.*} " + - "from ORGANIZATION org " + - " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .SetResultTransformer(new DistinctRootEntityResultTransformer()) - .ListAsync()); + l = await (s.CreateSQLQuery( + "select {org.*}, {emp.*} " + + "from ORGANIZATION org " + + " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .SetResultTransformer(new DistinctRootEntityResultTransformer()) + .ListAsync()); Assert.AreEqual(l.Count, 2); await (t.CommitAsync()); @@ -162,13 +163,13 @@ public async Task ResultSetMappingDefinitionAsync() await (s.SaveAsync(emp)); IList l = await (s.CreateSQLQuery(OrgEmpRegionSQL) - .SetResultSetMapping("org-emp-regionCode") - .ListAsync()); + .SetResultSetMapping("org-emp-regionCode") + .ListAsync()); Assert.AreEqual(l.Count, 2); l = await (s.CreateSQLQuery(OrgEmpPersonSQL) - .SetResultSetMapping("org-emp-person") - .ListAsync()); + .SetResultSetMapping("org-emp-person") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (s.DeleteAsync(emp)); @@ -313,7 +314,7 @@ public async Task MappedAliasStrategyAsync() sqlQuery.SetResultTransformer(CriteriaSpecification.AliasToEntityMap); list = await (sqlQuery.ListAsync()); Assert.AreEqual(2, list.Count); - m = (IDictionary)list[0]; + m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("org")); AssertClassAssignability(m["org"].GetType(), typeof(Organization)); Assert.IsTrue(m.Contains("emp")); @@ -381,28 +382,29 @@ public async Task CompositeIdJoinsFailureExpectedAsync() s = OpenSession(); t = s.BeginTransaction(); - object[] o = (object[]) (await (s.CreateSQLQuery("select\r\n" + - " product.orgid as {product.id.orgid}," + - " product.productnumber as {product.id.productnumber}," + - " {prod_orders}.orgid as orgid3_1_,\r\n" + - " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + - " product.name as {product.name}," + - " {prod_orders.element.*}," + - /*" orders.PROD_NO as PROD4_3_1_,\r\n" + - " orders.person as person3_1_,\r\n" + - " orders.PROD_ORGID as PROD3_0__,\r\n" + - " orders.PROD_NO as PROD4_0__,\r\n" + - " orders.orgid as orgid0__,\r\n" + - " orders.ordernumber as ordernum2_0__ \r\n" +*/ - " from\r\n" + - " Product product \r\n" + - " inner join\r\n" + - " TBL_ORDER {prod_orders} \r\n" + - " on product.orgid={prod_orders}.PROD_ORGID \r\n" + - " and product.productnumber={prod_orders}.PROD_NO") - .AddEntity("product", typeof(Product)) - .AddJoin("prod_orders", "product.orders") - .ListAsync()))[0]; + object[] o = (object[]) (await (s.CreateSQLQuery( + "select\r\n" + + " product.orgid as {product.id.orgid}," + + " product.productnumber as {product.id.productnumber}," + + " {prod_orders}.orgid as orgid3_1_,\r\n" + + " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + + " product.name as {product.name}," + + " {prod_orders.element.*}," + + /*" orders.PROD_NO as PROD4_3_1_,\r\n" + + " orders.person as person3_1_,\r\n" + + " orders.PROD_ORGID as PROD3_0__,\r\n" + + " orders.PROD_NO as PROD4_0__,\r\n" + + " orders.orgid as orgid0__,\r\n" + + " orders.ordernumber as ordernum2_0__ \r\n" +*/ + " from\r\n" + + " Product product \r\n" + + " inner join\r\n" + + " TBL_ORDER {prod_orders} \r\n" + + " on product.orgid={prod_orders}.PROD_ORGID \r\n" + + " and product.productnumber={prod_orders}.PROD_NO") + .AddEntity("product", typeof(Product)) + .AddJoin("prod_orders", "product.orders") + .ListAsync()))[0]; p = (Product) o[0]; Assert.IsTrue(NHibernateUtil.IsInitialized(p.Orders)); @@ -432,8 +434,8 @@ public async Task AutoDetectAliasingAsync() s = OpenSession(); t = s.BeginTransaction(); IList list = await (s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .ListAsync()); + .AddEntity(typeof(Employment).FullName) + .ListAsync()); Assert.AreEqual(1, list.Count); Employment emp2 = (Employment) list[0]; @@ -444,9 +446,9 @@ public async Task AutoDetectAliasingAsync() s.Clear(); list = await (s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) - .ListAsync()); + .AddEntity(typeof(Employment).FullName) + .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) + .ListAsync()); Assert.AreEqual(1, list.Count); IDictionary m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("Employment")); @@ -485,17 +487,17 @@ public async Task AutoDetectAliasingAsync() s.Clear(); list = await (s.CreateSQLQuery(OrganizationJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .ListAsync()); Assert.AreEqual(2, list.Count); s.Clear(); list = await (s.CreateSQLQuery(OrganizationFetchJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .ListAsync()); Assert.AreEqual(2, list.Count); s.Clear(); @@ -569,8 +571,8 @@ public async Task MixAndMatchEntityScalarAsync() s.Clear(); IList l = await (s.CreateSQLQuery("select name, id, flength, name as scalarName from Speech") - .SetResultSetMapping("speech") - .ListAsync()); + .SetResultSetMapping("speech") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (t.RollbackAsync()); @@ -583,9 +585,9 @@ public async Task ParameterListAsync() using (ISession s = OpenSession()) { IList l = await (s.CreateSQLQuery("select id from Speech where id in (:idList)") - .AddScalar("id", NHibernateUtil.Int32) - .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) - .ListAsync()); + .AddScalar("id", NHibernateUtil.Int32) + .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) + .ListAsync()); } } @@ -607,23 +609,26 @@ private double ExtractDoubleValue(object value) public static void AssertClassAssignability(System.Type source, System.Type target) { - Assert.IsTrue(target.IsAssignableFrom(source), - "Classes were not assignment-compatible : source<" + - source.FullName + - "> target<" + - target.FullName + ">" - ); + Assert.IsTrue( + target.IsAssignableFrom(source), + "Classes were not assignment-compatible : source<" + + source.FullName + + "> target<" + + target.FullName + ">" + ); } class TestResultSetTransformer : IResultTransformer { public bool TransformTupleCalled { get; set; } public bool TransformListCalled { get; set; } + public object TransformTuple(object[] tuple, string[] aliases) { this.TransformTupleCalled = true; return tuple; } + public IList TransformList(IList collection) { this.TransformListCalled = true; @@ -716,5 +721,33 @@ public async Task CanExecuteFutureValueAsync() Assert.AreEqual("Ricardo", v); } } + + [Test] + public async Task HandlesManualSynchronizationAsync() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + s.SessionFactory.Statistics.IsStatisticsEnabled = true; + s.SessionFactory.Statistics.Clear(); + + // create an Organization... + Organization jboss = new Organization("JBoss"); + await (s.PersistAsync(jboss)); + + // now query on Employment, this should not cause an auto-flush + await (s.CreateSQLQuery(EmploymentSQL).AddSynchronizedQuerySpace("ABC").ListAsync()); + Assert.AreEqual(0, s.SessionFactory.Statistics.EntityInsertCount); + + // now try to query on Employment but this time add Organization as a synchronized query space... + await (s.CreateSQLQuery(EmploymentSQL).AddSynchronizedEntityClass(typeof(Organization)).ListAsync()); + Assert.AreEqual(1, s.SessionFactory.Statistics.EntityInsertCount); + + // clean up + await (s.DeleteAsync(jboss)); + await (s.Transaction.CommitAsync()); + s.Close(); + } + } } } diff --git a/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs b/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs new file mode 100644 index 00000000000..53a099c753f --- /dev/null +++ b/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Action; +using NHibernate.Engine; +using NHibernate.Metadata; +using NHibernate.Persister.Entity; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Test.BulkManipulation +{ + [TestFixture] + public class BulkOperationCleanupActionFixture + { + private ISessionImplementor _session; + private ISessionFactoryImplementor _factory; + private IEntityPersister _persister; + + [SetUp] + public void SetupTest() + { + _session = Substitute.For(); + _factory = Substitute.For(); + _persister = Substitute.For(); + _session.Factory.Returns(_factory); + _factory.GetAllClassMetadata().Returns(new Dictionary { ["TestClass"] = null }); + _factory.GetEntityPersister("TestClass").Returns(_persister); + _factory.GetCollectionRolesByEntityParticipant("TestClass").Returns(new HashSet(new[] { "TestClass.Children" })); + _persister.QuerySpaces.Returns(new[] { "TestClass" }); + _persister.EntityName.Returns("TestClass"); + } + + [TestCase("TestClass", true, 1, 1, 1)] + [TestCase("AnotherClass", true, 1, 0, 0)] + [TestCase("AnotherClass,TestClass", true, 2, 1, 1)] + [TestCase("TestClass", false, 1, 0, 1)] + [TestCase("", true, 1, 1, 1)] + [Test] + // 6.0 TODO: remove this ignore. + [Ignore("Must wait for the tested methods to be actually added to ISessionFactoryImplementor")] + public void AfterTransactionCompletionProcess_EvictsFromCache(string querySpaces, bool persisterHasCache, int expectedPropertySpaceLength, int expectedEntityEvictionCount, int expectedCollectionEvictionCount) + { + _persister.HasCache.Returns(persisterHasCache); + + var target = new BulkOperationCleanupAction(_session, new HashSet(querySpaces.Split(new []{','},StringSplitOptions.RemoveEmptyEntries))); + + target.AfterTransactionCompletionProcess(true); + + Assert.AreEqual(expectedPropertySpaceLength, target.PropertySpaces.Length); + + if (expectedEntityEvictionCount > 0) + { + _factory.Received(1).EvictEntity(Arg.Is>(x => x.Count() == expectedEntityEvictionCount)); + } + else + { + _factory.DidNotReceive().EvictEntity(Arg.Any>()); + } + + if (expectedCollectionEvictionCount > 0) + { + _factory.Received(1).EvictCollection(Arg.Is>(x => x.Count() == expectedCollectionEvictionCount)); + } + else + { + _factory.DidNotReceive().EvictCollection(Arg.Any>()); + } + } + } +} diff --git a/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs b/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs new file mode 100644 index 00000000000..54b19b970aa --- /dev/null +++ b/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NSubstitute; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.BulkManipulation +{ + [TestFixture] + public class NativeSQLBulkOperationsWithCache : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "BulkManipulation.Vehicle.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.UseQueryCache, "true"); + cfg.SetProperty(Environment.UseSecondLevelCache, "true"); + cfg.SetProperty(Environment.CacheProvider, typeof(SubstituteCacheProvider).AssemblyQualifiedName); + } + + [Test] + public void SimpleNativeSQLInsert_DoesNotEvictEntireCacheWhenQuerySpacesAreAdded() + { + List clearCalls = new List(); + (Sfi.Settings.CacheProvider as SubstituteCacheProvider).OnClear(x => + { + clearCalls.Add(x); + }); + using (var s = OpenSession()) + { + string ssql = "UPDATE Vehicle SET Vin='123' WHERE Vin='123c'"; + + using (var t = s.BeginTransaction()) + { + + s.CreateSQLQuery(ssql).ExecuteUpdate(); + t.Commit(); + + Assert.AreEqual(1, clearCalls.Count); + } + + clearCalls.Clear(); + + using (var t = s.BeginTransaction()) + { + s.CreateSQLQuery(ssql).AddSynchronizedQuerySpace("Unknown").ExecuteUpdate(); + t.Commit(); + + Assert.AreEqual(0, clearCalls.Count); + } + } + } + } + + public class SubstituteCacheProvider : ICacheProvider + { + private readonly ConcurrentDictionary> _caches = new ConcurrentDictionary>(); + private Action _onClear; + + public ICache BuildCache(string regionName, IDictionary properties) + { + return _caches.GetOrAdd(regionName, x => new Lazy(() => + { + var cache = Substitute.For(); + cache.RegionName.Returns(regionName); + cache.When(c => c.Clear()).Do(c => _onClear?.Invoke(regionName)); + return cache; + })).Value; + } + + public long NextTimestamp() + { + return Timestamper.Next(); + } + + public void Start(IDictionary properties) + { + } + + public void Stop() + { + } + + public ICache GetCache(string region) + { + Lazy cache; + _caches.TryGetValue(region, out cache); + return cache?.Value; + } + + public IEnumerable GetAllCaches() + { + return _caches.Values.Select(x => x.Value); + } + + public void OnClear(Action callback) + { + _onClear = callback; + } + } +} diff --git a/src/NHibernate.Test/MappingByCode/For.cs b/src/NHibernate.Test/MappingByCode/For.cs index 46f76f8b05b..1ab97920ec0 100644 --- a/src/NHibernate.Test/MappingByCode/For.cs +++ b/src/NHibernate.Test/MappingByCode/For.cs @@ -16,4 +16,4 @@ public static MemberInfo Property(Expression> propertyGetter) return TypeExtensions.DecodeMemberAccessExpression(propertyGetter); } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 3ae3f734cda..6e6df5e293d 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs index 69cc26b6ee3..f655d771cb9 100644 --- a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -43,7 +43,7 @@ public class GeneralTest : TestCase protected override IList Mappings { - get { return new[] { "SqlTest.Query.NativeSQLQueries.hbm.xml" }; } + get { return new[] {"SqlTest.Query.NativeSQLQueries.hbm.xml"}; } } protected override string MappingsAssembly @@ -92,17 +92,17 @@ public void SQLQueryInterface() s.Save(emp); IList l = s.CreateSQLQuery(OrgEmpRegionSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddScalar("regionCode", NHibernateUtil.String) - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddScalar("regionCode", NHibernateUtil.String) + .List(); Assert.AreEqual(2, l.Count); l = s.CreateSQLQuery(OrgEmpPersonSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddJoin("pers", "emp.employee") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddJoin("pers", "emp.employee") + .List(); Assert.AreEqual(l.Count, 1); t.Commit(); @@ -111,13 +111,14 @@ public void SQLQueryInterface() s = OpenSession(); t = s.BeginTransaction(); - l = s.CreateSQLQuery("select {org.*}, {emp.*} " + - "from ORGANIZATION org " + - " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .SetResultTransformer(new DistinctRootEntityResultTransformer()) - .List(); + l = s.CreateSQLQuery( + "select {org.*}, {emp.*} " + + "from ORGANIZATION org " + + " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .SetResultTransformer(new DistinctRootEntityResultTransformer()) + .List(); Assert.AreEqual(l.Count, 2); t.Commit(); @@ -151,13 +152,13 @@ public void ResultSetMappingDefinition() s.Save(emp); IList l = s.CreateSQLQuery(OrgEmpRegionSQL) - .SetResultSetMapping("org-emp-regionCode") - .List(); + .SetResultSetMapping("org-emp-regionCode") + .List(); Assert.AreEqual(l.Count, 2); l = s.CreateSQLQuery(OrgEmpPersonSQL) - .SetResultSetMapping("org-emp-person") - .List(); + .SetResultSetMapping("org-emp-person") + .List(); Assert.AreEqual(l.Count, 1); s.Delete(emp); @@ -302,7 +303,7 @@ public void MappedAliasStrategy() sqlQuery.SetResultTransformer(CriteriaSpecification.AliasToEntityMap); list = sqlQuery.List(); Assert.AreEqual(2, list.Count); - m = (IDictionary)list[0]; + m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("org")); AssertClassAssignability(m["org"].GetType(), typeof(Organization)); Assert.IsTrue(m.Contains("emp")); @@ -370,28 +371,29 @@ public void CompositeIdJoinsFailureExpected() s = OpenSession(); t = s.BeginTransaction(); - object[] o = (object[]) s.CreateSQLQuery("select\r\n" + - " product.orgid as {product.id.orgid}," + - " product.productnumber as {product.id.productnumber}," + - " {prod_orders}.orgid as orgid3_1_,\r\n" + - " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + - " product.name as {product.name}," + - " {prod_orders.element.*}," + - /*" orders.PROD_NO as PROD4_3_1_,\r\n" + - " orders.person as person3_1_,\r\n" + - " orders.PROD_ORGID as PROD3_0__,\r\n" + - " orders.PROD_NO as PROD4_0__,\r\n" + - " orders.orgid as orgid0__,\r\n" + - " orders.ordernumber as ordernum2_0__ \r\n" +*/ - " from\r\n" + - " Product product \r\n" + - " inner join\r\n" + - " TBL_ORDER {prod_orders} \r\n" + - " on product.orgid={prod_orders}.PROD_ORGID \r\n" + - " and product.productnumber={prod_orders}.PROD_NO") - .AddEntity("product", typeof(Product)) - .AddJoin("prod_orders", "product.orders") - .List()[0]; + object[] o = (object[]) s.CreateSQLQuery( + "select\r\n" + + " product.orgid as {product.id.orgid}," + + " product.productnumber as {product.id.productnumber}," + + " {prod_orders}.orgid as orgid3_1_,\r\n" + + " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + + " product.name as {product.name}," + + " {prod_orders.element.*}," + + /*" orders.PROD_NO as PROD4_3_1_,\r\n" + + " orders.person as person3_1_,\r\n" + + " orders.PROD_ORGID as PROD3_0__,\r\n" + + " orders.PROD_NO as PROD4_0__,\r\n" + + " orders.orgid as orgid0__,\r\n" + + " orders.ordernumber as ordernum2_0__ \r\n" +*/ + " from\r\n" + + " Product product \r\n" + + " inner join\r\n" + + " TBL_ORDER {prod_orders} \r\n" + + " on product.orgid={prod_orders}.PROD_ORGID \r\n" + + " and product.productnumber={prod_orders}.PROD_NO") + .AddEntity("product", typeof(Product)) + .AddJoin("prod_orders", "product.orders") + .List()[0]; p = (Product) o[0]; Assert.IsTrue(NHibernateUtil.IsInitialized(p.Orders)); @@ -421,8 +423,8 @@ public void AutoDetectAliasing() s = OpenSession(); t = s.BeginTransaction(); IList list = s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .List(); + .AddEntity(typeof(Employment).FullName) + .List(); Assert.AreEqual(1, list.Count); Employment emp2 = (Employment) list[0]; @@ -433,9 +435,9 @@ public void AutoDetectAliasing() s.Clear(); list = s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) - .List(); + .AddEntity(typeof(Employment).FullName) + .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) + .List(); Assert.AreEqual(1, list.Count); IDictionary m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("Employment")); @@ -474,17 +476,17 @@ public void AutoDetectAliasing() s.Clear(); list = s.CreateSQLQuery(OrganizationJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .List(); Assert.AreEqual(2, list.Count); s.Clear(); list = s.CreateSQLQuery(OrganizationFetchJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .List(); Assert.AreEqual(2, list.Count); s.Clear(); @@ -558,8 +560,8 @@ public void MixAndMatchEntityScalar() s.Clear(); IList l = s.CreateSQLQuery("select name, id, flength, name as scalarName from Speech") - .SetResultSetMapping("speech") - .List(); + .SetResultSetMapping("speech") + .List(); Assert.AreEqual(l.Count, 1); t.Rollback(); @@ -572,9 +574,9 @@ public void ParameterList() using (ISession s = OpenSession()) { IList l = s.CreateSQLQuery("select id from Speech where id in (:idList)") - .AddScalar("id", NHibernateUtil.Int32) - .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) - .List(); + .AddScalar("id", NHibernateUtil.Int32) + .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) + .List(); } } @@ -596,23 +598,26 @@ private double ExtractDoubleValue(object value) public static void AssertClassAssignability(System.Type source, System.Type target) { - Assert.IsTrue(target.IsAssignableFrom(source), - "Classes were not assignment-compatible : source<" + - source.FullName + - "> target<" + - target.FullName + ">" - ); + Assert.IsTrue( + target.IsAssignableFrom(source), + "Classes were not assignment-compatible : source<" + + source.FullName + + "> target<" + + target.FullName + ">" + ); } class TestResultSetTransformer : IResultTransformer { public bool TransformTupleCalled { get; set; } public bool TransformListCalled { get; set; } + public object TransformTuple(object[] tuple, string[] aliases) { this.TransformTupleCalled = true; return tuple; } + public IList TransformList(IList collection) { this.TransformListCalled = true; @@ -705,5 +710,33 @@ public void CanExecuteFutureValue() Assert.AreEqual("Ricardo", v); } } + + [Test] + public void HandlesManualSynchronization() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + s.SessionFactory.Statistics.IsStatisticsEnabled = true; + s.SessionFactory.Statistics.Clear(); + + // create an Organization... + Organization jboss = new Organization("JBoss"); + s.Persist(jboss); + + // now query on Employment, this should not cause an auto-flush + s.CreateSQLQuery(EmploymentSQL).AddSynchronizedQuerySpace("ABC").List(); + Assert.AreEqual(0, s.SessionFactory.Statistics.EntityInsertCount); + + // now try to query on Employment but this time add Organization as a synchronized query space... + s.CreateSQLQuery(EmploymentSQL).AddSynchronizedEntityClass(typeof(Organization)).List(); + Assert.AreEqual(1, s.SessionFactory.Statistics.EntityInsertCount); + + // clean up + s.Delete(jboss); + s.Transaction.Commit(); + s.Close(); + } + } } } diff --git a/src/NHibernate.sln b/src/NHibernate.sln index 67d957ed5d6..2c7f1c7eb97 100644 --- a/src/NHibernate.sln +++ b/src/NHibernate.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{593DCEA7-C933-46F3-939F-D8172399AB05}" ProjectSection(SolutionItems) = preProject diff --git a/src/NHibernate/Action/BulkOperationCleanupAction.cs b/src/NHibernate/Action/BulkOperationCleanupAction.cs index 1e6edd1d0b3..53cbaf177bc 100644 --- a/src/NHibernate/Action/BulkOperationCleanupAction.cs +++ b/src/NHibernate/Action/BulkOperationCleanupAction.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NHibernate.Engine; using NHibernate.Metadata; using NHibernate.Persister.Entity; +using IQueryable = NHibernate.Persister.Entity.IQueryable; namespace NHibernate.Action { @@ -10,7 +14,7 @@ namespace NHibernate.Action /// Implementation of BulkOperationCleanupAction. /// [Serializable] - public partial class BulkOperationCleanupAction: IExecutable + public partial class BulkOperationCleanupAction : IExecutable { private readonly ISessionImplementor session; private readonly HashSet affectedEntityNames = new HashSet(); @@ -84,14 +88,8 @@ private bool AffectedEntity(ISet querySpaces, string[] entitySpaces) return true; } - for (int i = 0; i < entitySpaces.Length; i++) - { - if (querySpaces.Contains(entitySpaces[i])) - { - return true; - } - } - return false; + + return entitySpaces.Any(querySpaces.Contains); } #region IExecutable Members @@ -113,8 +111,8 @@ public void Execute() public BeforeTransactionCompletionProcessDelegate BeforeTransactionCompletionProcess { - get - { + get + { return null; } } @@ -133,32 +131,36 @@ public AfterTransactionCompletionProcessDelegate AfterTransactionCompletionProce private void EvictCollectionRegions() { - if (affectedCollectionRoles != null) + if (affectedCollectionRoles != null && affectedCollectionRoles.Any()) { - foreach (string roleName in affectedCollectionRoles) - { - session.Factory.EvictCollection(roleName); - } + session.Factory.EvictCollection(affectedCollectionRoles); } } private void EvictEntityRegions() { - if (affectedEntityNames != null) + if (affectedEntityNames != null && affectedEntityNames.Any()) { - foreach (string entityName in affectedEntityNames) - { - session.Factory.EvictEntity(entityName); - } + session.Factory.EvictEntity(affectedEntityNames); } } #endregion + // Since v5.2 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] public virtual void Init() { EvictEntityRegions(); EvictCollectionRegions(); } + + // Since v5.2 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] + public virtual async Task InitAsync(CancellationToken cancellationToken) + { + await EvictEntityRegionsAsync(cancellationToken); + await EvictCollectionRegionsAsync(cancellationToken); + } } } diff --git a/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs b/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs index 899db3bc5d0..c65a41770fc 100644 --- a/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs +++ b/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs @@ -10,15 +10,17 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NHibernate.Engine; using NHibernate.Metadata; using NHibernate.Persister.Entity; +using IQueryable = NHibernate.Persister.Entity.IQueryable; namespace NHibernate.Action { - using System.Threading.Tasks; - using System.Threading; - public partial class BulkOperationCleanupAction: IExecutable + public partial class BulkOperationCleanupAction : IExecutable { #region IExecutable Members @@ -57,37 +59,46 @@ public Task ExecuteAsync(CancellationToken cancellationToken) } } - private async Task EvictCollectionRegionsAsync(CancellationToken cancellationToken) + private Task EvictCollectionRegionsAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - if (affectedCollectionRoles != null) + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try { - foreach (string roleName in affectedCollectionRoles) + if (affectedCollectionRoles != null && affectedCollectionRoles.Any()) { - await (session.Factory.EvictCollectionAsync(roleName, cancellationToken)).ConfigureAwait(false); + return session.Factory.EvictCollectionAsync(affectedCollectionRoles, cancellationToken); } + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } - private async Task EvictEntityRegionsAsync(CancellationToken cancellationToken) + private Task EvictEntityRegionsAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - if (affectedEntityNames != null) + if (cancellationToken.IsCancellationRequested) { - foreach (string entityName in affectedEntityNames) + return Task.FromCanceled(cancellationToken); + } + try + { + if (affectedEntityNames != null && affectedEntityNames.Any()) { - await (session.Factory.EvictEntityAsync(entityName, cancellationToken)).ConfigureAwait(false); + return session.Factory.EvictEntityAsync(affectedEntityNames, cancellationToken); } + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } #endregion - - public virtual async Task InitAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - await (EvictEntityRegionsAsync(cancellationToken)).ConfigureAwait(false); - await (EvictCollectionRegionsAsync(cancellationToken)).ConfigureAwait(false); - } } } diff --git a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs index 2cf63ed75c9..19edd8b92ca 100644 --- a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs +++ b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs @@ -34,24 +34,11 @@ namespace NHibernate.Engine.Query public partial class NativeSQLQueryPlan { - private async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - BulkOperationCleanupAction action = new BulkOperationCleanupAction(session, CustomQuery.QuerySpaces); - - await (action.InitAsync(cancellationToken)).ConfigureAwait(false); - - if (session.IsEventSource) - { - ((IEventSource)session).ActionQueue.AddAction(action); - } - } - // DONE : H3.2 Executable query (now can be supported for named SQL query/ storedProcedure) public async Task PerformExecuteUpdateAsync(QueryParameters queryParameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); if (queryParameters.Callable) { diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs index f3300c08454..046ff3d4c51 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs @@ -24,29 +24,16 @@ using NHibernate.Transaction; using NHibernate.Util; using System.Data; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Hql.Ast.ANTLR.Exec { - using System.Threading.Tasks; - using System.Threading; public abstract partial class AbstractStatementExecutor : IStatementExecutor { public abstract Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken); - protected virtual async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var action = new BulkOperationCleanupAction(session, AffectedQueryables); - - await (action.InitAsync(cancellationToken)).ConfigureAwait(false); - - if (session.IsEventSource) - { - ((IEventSource)session).ActionQueue.AddAction(action); - } - } - protected virtual async Task CreateTemporaryTableIfNecessaryAsync(IQueryable persister, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs index b5f4fd04194..62499dcdd5c 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs @@ -34,7 +34,7 @@ public partial class BasicExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); DbCommand st = null; RowSelection selection = parameters.RowSelection; diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs index 27146fd0f95..2377dfb8ba3 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs @@ -31,7 +31,7 @@ public partial class MultiTableDeleteExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); await (CreateTemporaryTableIfNecessaryAsync(persister, session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs index 71de9bb7c59..a7b4c996647 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs @@ -33,7 +33,7 @@ public partial class MultiTableUpdateExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); await (CreateTemporaryTableIfNecessaryAsync(persister, session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/ISessionFactory.cs b/src/NHibernate/Async/ISessionFactory.cs index aea30be06be..744c59b3bde 100644 --- a/src/NHibernate/Async/ISessionFactory.cs +++ b/src/NHibernate/Async/ISessionFactory.cs @@ -14,6 +14,7 @@ using System.Data.Common; using NHibernate.Connection; using NHibernate.Engine; +using NHibernate.Impl; using NHibernate.Metadata; using NHibernate.Stat; @@ -21,6 +22,87 @@ namespace NHibernate { using System.Threading.Tasks; using System.Threading; + public static partial class SessionFactoryExtension + { + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The classes of the entities to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictAsync(this ISessionFactory factory, IEnumerable persistentClasses, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictAsync(persistentClasses, cancellationToken)).ConfigureAwait(false); + } + else + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + foreach (var @class in persistentClasses) + { + await (factory.EvictAsync(@class, cancellationToken)).ConfigureAwait(false); + } + } + } + + /// + /// Evict all entries from the second-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the entities to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictEntityAsync(this ISessionFactory factory, IEnumerable entityNames, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictEntityAsync(entityNames, cancellationToken)).ConfigureAwait(false); + } + else + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + foreach (var name in entityNames) + { + await (factory.EvictEntityAsync(name, cancellationToken)).ConfigureAwait(false); + } + } + } + + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the collections to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictCollectionAsync(this ISessionFactory factory, IEnumerable roleNames, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictCollectionAsync(roleNames, cancellationToken)).ConfigureAwait(false); + } + else + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + foreach (var role in roleNames) + { + await (factory.EvictCollectionAsync(role, cancellationToken)).ConfigureAwait(false); + } + } + } + } + public partial interface ISessionFactory : IDisposable { diff --git a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs index 05b23b537ea..8dcb9b347da 100644 --- a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs @@ -165,6 +165,24 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictAsync(IEnumerable persistentClasses, CancellationToken cancellationToken) + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return EvictEntityAsync(persistentClasses.Select(x => x.FullName), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + public Task EvictEntityAsync(string entityName, CancellationToken cancellationToken = default(CancellationToken)) { if (cancellationToken.IsCancellationRequested) @@ -190,6 +208,30 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictEntityAsync(IEnumerable entityNames, CancellationToken cancellationToken) + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalEvictEntityAsync(); + async Task InternalEvictEntityAsync() + { + + foreach (var cacheGroup in entityNames.Select(GetEntityPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.EntityName))); + } + await (cacheGroup.Key.ClearAsync(cancellationToken)).ConfigureAwait(false); + } + } + } + public Task EvictEntityAsync(string entityName, object id, CancellationToken cancellationToken = default(CancellationToken)) { if (cancellationToken.IsCancellationRequested) @@ -267,6 +309,30 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictCollectionAsync(IEnumerable roleNames, CancellationToken cancellationToken) + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalEvictCollectionAsync(); + async Task InternalEvictCollectionAsync() + { + + foreach (var cacheGroup in roleNames.Select(GetCollectionPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.Role))); + } + await (cacheGroup.Key.ClearAsync(cancellationToken)).ConfigureAwait(false); + } + } + } + public async Task EvictQueriesAsync(CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Impl/SqlQueryImpl.cs b/src/NHibernate/Async/Impl/SqlQueryImpl.cs index 8f85915879a..6811ce34792 100644 --- a/src/NHibernate/Async/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Async/Impl/SqlQueryImpl.cs @@ -21,7 +21,7 @@ namespace NHibernate.Impl { using System.Threading.Tasks; using System.Threading; - public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery + public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizableSQLQuery { public override async Task ListAsync(CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs b/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs index f2e9a275f45..2d71b63bfe2 100644 --- a/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs +++ b/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs @@ -48,8 +48,6 @@ private void CoordinateSharedCacheCleanup(ISessionImplementor session) { BulkOperationCleanupAction action = new BulkOperationCleanupAction(session, CustomQuery.QuerySpaces); - action.Init(); - if (session.IsEventSource) { ((IEventSource)session).ActionQueue.AddAction(action); diff --git a/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs b/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs index a45819d7c6c..8b864f8da1d 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs @@ -14,6 +14,8 @@ using NHibernate.Transaction; using NHibernate.Util; using System.Data; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Hql.Ast.ANTLR.Exec { @@ -47,11 +49,30 @@ protected virtual void CoordinateSharedCacheCleanup(ISessionImplementor session) { var action = new BulkOperationCleanupAction(session, AffectedQueryables); - action.Init(); - if (session.IsEventSource) { - ((IEventSource)session).ActionQueue.AddAction(action); + ((IEventSource) session).ActionQueue.AddAction(action); + } + else + { + action.AfterTransactionCompletionProcess(true); + } + } + + // Since v5.2 + [Obsolete("This method has no more actual async calls to do, use its sync version instead.")] + protected virtual Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + CoordinateSharedCacheCleanup(session); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } diff --git a/src/NHibernate/ISQLQuery.cs b/src/NHibernate/ISQLQuery.cs index 5e8ea66dacd..cb28cac13d0 100644 --- a/src/NHibernate/ISQLQuery.cs +++ b/src/NHibernate/ISQLQuery.cs @@ -1,7 +1,66 @@ +using System; +using System.Collections.Generic; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate { + // 6.0 TODO: remove after having done ISynchronizableSQLQuery todo + public static class SQLQueryExtension + { + /// + /// Adds a query space for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The query space. + /// The query. + public static ISQLQuery AddSynchronizedQuerySpace(this ISQLQuery sqlQuery, string querySpace) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedQuerySpace(querySpace); + } + + /// + /// Adds an entity name for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The entity name. + /// The query. + public static ISQLQuery AddSynchronizedEntityName(this ISQLQuery sqlQuery, string entityName) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedEntityName(entityName); + } + + /// + /// Adds an entity type for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The entity type. + /// The query. + public static ISQLQuery AddSynchronizedEntityClass(this ISQLQuery sqlQuery, System.Type entityType) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedEntityClass(entityType); + } + + /// + /// Returns the synchronized query spaces added to the query. + /// + /// The query. + /// The synchronized query spaces. + public static IReadOnlyCollection GetSynchronizedQuerySpaces(this ISQLQuery sqlQuery) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.GetSynchronizedQuerySpaces(); + } + } + + // 6.0 TODO: obsolete ISynchronizableSQLQuery and have ISQLQuery directly extending ISynchronizableQuery + public interface ISynchronizableSQLQuery : ISQLQuery, ISynchronizableQuery + { + } + public interface ISQLQuery : IQuery { /// diff --git a/src/NHibernate/ISessionFactory.cs b/src/NHibernate/ISessionFactory.cs index 6881abf59aa..544f79d1a57 100644 --- a/src/NHibernate/ISessionFactory.cs +++ b/src/NHibernate/ISessionFactory.cs @@ -4,11 +4,88 @@ using System.Data.Common; using NHibernate.Connection; using NHibernate.Engine; +using NHibernate.Impl; using NHibernate.Metadata; using NHibernate.Stat; namespace NHibernate { + // 6.0 TODO: move below methods directly in ISessionFactory then remove SessionFactoryExtension + public static partial class SessionFactoryExtension + { + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The classes of the entities to evict. + public static void Evict(this ISessionFactory factory, IEnumerable persistentClasses) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.Evict(persistentClasses); + } + else + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + foreach (var @class in persistentClasses) + { + factory.Evict(@class); + } + } + } + + /// + /// Evict all entries from the second-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the entities to evict. + public static void EvictEntity(this ISessionFactory factory, IEnumerable entityNames) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.EvictEntity(entityNames); + } + else + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + foreach (var name in entityNames) + { + factory.EvictEntity(name); + } + } + } + + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the collections to evict. + public static void EvictCollection(this ISessionFactory factory, IEnumerable roleNames) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.EvictCollection(roleNames); + } + else + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + foreach (var role in roleNames) + { + factory.EvictCollection(role); + } + } + } + } + /// /// Creates ISessions. /// diff --git a/src/NHibernate/ISynchronizableQuery.cs b/src/NHibernate/ISynchronizableQuery.cs new file mode 100644 index 00000000000..03535fea750 --- /dev/null +++ b/src/NHibernate/ISynchronizableQuery.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace NHibernate +{ + public interface ISynchronizableQuery where T : ISynchronizableQuery + { + /// + /// Adds a query space for auto-flush synchronization and second level cache invalidation. + /// + /// The query space. + /// The query. + T AddSynchronizedQuerySpace(string querySpace); + + /// + /// Adds an entity name for auto-flush synchronization and second level cache invalidation. + /// + /// The entity name. + /// The query. + T AddSynchronizedEntityName(string entityName); + + /// + /// Adds an entity type for auto-flush synchronization and second level cache invalidation. + /// + /// The entity type. + /// The query. + T AddSynchronizedEntityClass(System.Type entityType); + + /// + /// Returns the synchronized query spaces added to the query. + /// + /// The synchronized query spaces. + IReadOnlyCollection GetSynchronizedQuerySpaces(); + } +} diff --git a/src/NHibernate/Impl/AbstractQueryImpl.cs b/src/NHibernate/Impl/AbstractQueryImpl.cs index 4331813c028..f77e4b4a1cf 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl.cs @@ -18,7 +18,7 @@ namespace NHibernate.Impl public abstract partial class AbstractQueryImpl : IQuery { private readonly string queryString; - private readonly ISessionImplementor session; + protected readonly ISessionImplementor session; protected internal ParameterMetadata parameterMetadata; private readonly RowSelection selection; diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index d80c1a1fdb7..a715723f71e 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -901,6 +901,13 @@ public void Evict(System.Type persistentClass) } } + public void Evict(IEnumerable persistentClasses) + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + EvictEntity(persistentClasses.Select(x => x.FullName)); + } + public void EvictEntity(string entityName) { IEntityPersister p = GetEntityPersister(entityName); @@ -914,6 +921,22 @@ public void EvictEntity(string entityName) } } + public void EvictEntity(IEnumerable entityNames) + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + + foreach (var cacheGroup in entityNames.Select(GetEntityPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.EntityName))); + } + cacheGroup.Key.Clear(); + } + } + public void EvictEntity(string entityName, object id) { IEntityPersister p = GetEntityPersister(entityName); @@ -969,6 +992,22 @@ public void EvictCollection(string roleName) } } + public void EvictCollection(IEnumerable roleNames) + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + + foreach (var cacheGroup in roleNames.Select(GetCollectionPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.Role))); + } + cacheGroup.Key.Clear(); + } + } + public IType GetReferencedPropertyType(string className, string propertyName) { return GetEntityPersister(className).GetPropertyType(propertyName); diff --git a/src/NHibernate/Impl/SqlQueryImpl.cs b/src/NHibernate/Impl/SqlQueryImpl.cs index 43bb2544500..0cc008d2173 100644 --- a/src/NHibernate/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Impl/SqlQueryImpl.cs @@ -22,12 +22,13 @@ namespace NHibernate.Impl /// </sql-query-name> /// /// - public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery + public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizableSQLQuery { private readonly IList queryReturns; private readonly ICollection querySpaces; private readonly bool callable; private bool autoDiscoverTypes; + private readonly HashSet addedQuerySpaces = new HashSet(); /// Constructs a SQLQueryImpl given a sql query defined in the mappings. /// The representation of the defined sql-query. @@ -170,10 +171,15 @@ public override IList List() public NativeSQLQuerySpecification GenerateQuerySpecification(IDictionary parameters) { + var allQuerySpaces = new List(GetSynchronizedQuerySpaces()); + if (querySpaces != null) + { + allQuerySpaces.AddRange(querySpaces); + } return new NativeSQLQuerySpecification( ExpandParameterLists(parameters), GetQueryReturns(), - querySpaces); + allQuerySpaces); } public override QueryParameters GetQueryParameters(IDictionary namedParams) @@ -320,5 +326,36 @@ protected internal override IEnumerable GetTranslators(ISessionImpl var sqlQuery = this as ISQLQuery; yield return new SqlTranslator(sqlQuery, sessionImplementor.Factory); } + + public ISynchronizableSQLQuery AddSynchronizedQuerySpace(string querySpace) + { + addedQuerySpaces.Add(querySpace); + return this; + } + + public ISynchronizableSQLQuery AddSynchronizedEntityName(string entityName) + { + var persister = session.Factory.GetEntityPersister(entityName); + foreach (var querySpace in persister.QuerySpaces) + { + addedQuerySpaces.Add(querySpace); + } + return this; + } + + public ISynchronizableSQLQuery AddSynchronizedEntityClass(System.Type entityType) + { + var persister = session.Factory.GetEntityPersister(entityType.FullName); + foreach (var querySpace in persister.QuerySpaces) + { + addedQuerySpaces.Add(querySpace); + } + return this; + } + + public IReadOnlyCollection GetSynchronizedQuerySpaces() + { + return addedQuerySpaces; + } } }