diff --git a/Tools/packages.config b/Tools/packages.config index e54c238eca4..2a94e5aa85e 100644 --- a/Tools/packages.config +++ b/Tools/packages.config @@ -7,5 +7,5 @@ - + diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index 06f4f4cf3ec..3e994729866 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -42,6 +42,28 @@ using NHibernate.Linq;]]> .Where(c => c.Name == "Max") .ToList();]]> + + Starting with NHibernate 5.0, queries can also be created from an entity collection, with the standard + Linq extension AsQueryable available from System.Linq namespace. + + whiteKittens = + cat.Kittens.AsQueryable() + .Where(k => k.Color == "white") + .ToList();]]> + + This will be executed as a query on that cat's kittens without loading the + entire collection. + + + If the collection is a map, call AsQueryable on its Values + property. + + whiteKittens = + cat.Kittens.Values.AsQueryable() + .Where(k => k.Color == "white") + .ToList();]]> +   + A client timeout for the query can be defined. @@ -789,4 +811,4 @@ cfg.LinqToHqlGeneratorsRegistry(); - \ No newline at end of file + diff --git a/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs index aa3c2db5793..10bccd2305f 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs @@ -72,6 +72,7 @@ public class TextLogSpy : IDisposable private readonly TextWriterAppender appender; private readonly Logger loggerImpl; private readonly StringBuilder stringBuilder; + private readonly Level previousLevel; public TextLogSpy(string loggerName, string pattern) { @@ -84,6 +85,7 @@ public TextLogSpy(string loggerName, string pattern) }; loggerImpl = (Logger)LogManager.GetLogger(typeof(LogsFixtureAsync).Assembly, loggerName).Logger; loggerImpl.AddAppender(appender); + previousLevel = loggerImpl.Level; loggerImpl.Level = Level.All; } @@ -98,9 +100,10 @@ public string[] Events public void Dispose() { loggerImpl.RemoveAppender(appender); + loggerImpl.Level = previousLevel; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs index 03286bacbc9..39732200737 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs @@ -51,8 +51,8 @@ public async Task LoadCompositeElementsWithWithSimpleHbmAliasInjectionAsync() Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithSimpleHbmAliasInjection")); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); await (CleanupWithPersonsAsync()); } @@ -63,8 +63,8 @@ public async Task LoadCompositeElementsWithWithComplexHbmAliasInjectionAsync() Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithComplexHbmAliasInjection")); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); await (CleanupWithPersonsAsync()); } @@ -75,8 +75,8 @@ public async Task LoadCompositeElementsWithWithCustomAliasesAsync() Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithCustomAliases")); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); await (CleanupWithPersonsAsync()); } @@ -201,8 +201,8 @@ public async Task LoadCompositeElementCollectionWithCustomLoaderAsync() { var a = await (session.GetAsync(country.Code)); Assert.That(a, Is.Not.Null, "area"); - Assert.That((ICollection) a.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "area.Keys"); - Assert.That((ICollection) a.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "area.Elements"); + Assert.That(a.Statistics.Keys, Is.EquivalentTo(stats.Keys), "area.Keys"); + Assert.That(a.Statistics.Values, Is.EquivalentTo(stats.Values), "area.Elements"); } await (CleanupWithPersonsAsync()); } @@ -428,4 +428,4 @@ private static IDictionary CreateStatistics() #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs new file mode 100644 index 00000000000..68dc36629f3 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs @@ -0,0 +1,741 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Collection; +using NHibernate.Engine.Query; +using NHibernate.Mapping.ByCode; +using NHibernate.Util; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.NH2319 +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public abstract class FixtureBaseAsync : TestCaseMappingByCode + { + private Guid _parent1Id; + private Guid _child1Id; + private Guid _parent2Id; + private Guid _child3Id; + + [Test] + public Task ShouldBeAbleToFindChildrenByNameAsync() + { + return FindChildrenByNameAsync(_parent1Id, _child1Id); + } + + private async Task FindChildrenByNameAsync(Guid parentId, Guid childId, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(parentId, cancellationToken)); + + Assert.That(parent, Is.Not.Null); + + var filtered = await (parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync(cancellationToken)); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(filtered[0].Id, Is.EqualTo(childId)); + } + } + + [Test] + public async Task ShouldBeAbleToPerformComplexFilteringAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + + Assert.NotNull(parent); + + var filtered = await (parent.Children + .AsQueryable() + .Where(x => x.Name == "Piter") + .SelectMany(x => x.GrandChildren) + .Select(x => x.Id) + .CountAsync()); + + Assert.That(filtered, Is.EqualTo(2)); + } + } + + [Test] + public async Task ShouldBeAbleToReuseQueryPlanAsync() + { + await (ShouldBeAbleToFindChildrenByNameAsync()); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + Assert.That(ShouldBeAbleToFindChildrenByNameAsync, Throws.Nothing); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public async Task ShouldNotMixResultsAsync() + { + await (FindChildrenByNameAsync(_parent1Id, _child1Id)); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + await (FindChildrenByNameAsync(_parent2Id, _child3Id)); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public async Task ShouldNotInitializeCollectionWhenPerformingQueryAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var persistentCollection = (IPersistentCollection) parent.Children; + + var filtered = await (parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync()); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(persistentCollection.WasInitialized, Is.False); + } + } + + [Test] + public async Task ShouldPerformSqlQueryEvenIfCollectionAlreadyInitializedAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var loaded = parent.Children.ToList(); + Assert.That(loaded, Has.Count.EqualTo(2)); + + var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + var filtered = await (parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync()); + + var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1)); + } + } + + [Test] + public async Task TestFilterAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var children = await ((await (session.CreateFilterAsync(parent.Children, "where this.Name = 'Jack'"))) + .ListAsync()); + + Assert.That(children, Has.Count.EqualTo(1)); + } + } + + [Test] + public async Task TestPlanCacheMissAsync() + { + var internalPlanCache = typeof(QueryPlanCache) + .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache; + Assert.That(internalPlanCache, Is.Not.Null, + $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation."); + + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + internalPlanCache.Clear(); + await (ShouldBeAbleToFindChildrenByNameAsync()); + AssertFilterPlanCacheMiss(spy); + } + } + + private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache"; + + private static void AssertFilterPlanCacheHit(LogSpy spy) => + // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed + // rather than only asserting it has been hit. + Assert.That(spy.GetWholeLog(), + Contains.Substring("located collection-filter query plan in cache (") + .And.Not.Contains(_filterPlanCacheMissLog)); + + private static void AssertFilterPlanCacheMiss(LogSpy spy) => + Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog)); + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("show_sql", "true"); + configuration.SetProperty("generate_statistics", "true"); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent1 = new Parent { Name = "Bob" }; + _parent1Id = (Guid) session.Save(parent1); + + var parent2 = new Parent { Name = "Martin" }; + _parent2Id = (Guid) session.Save(parent2); + + var child1 = new Child + { + Name = "Jack", + Parent = parent1 + }; + parent1.Children.Add(child1); + child1.Parents.Add(parent1); + _child1Id = (Guid) session.Save(child1); + + var child2 = new Child + { + Name = "Piter", + Parent = parent1 + }; + parent1.Children.Add(child2); + child2.Parents.Add(parent1); + session.Save(child2); + + var grandChild1 = new GrandChild + { + Name = "Kate", + Child = child2 + }; + child2.GrandChildren.Add(grandChild1); + grandChild1.ParentChidren.Add(child2); + session.Save(grandChild1); + + var grandChild2 = new GrandChild + { + Name = "Mary", + Child = child2 + }; + child2.GrandChildren.Add(grandChild2); + grandChild2.ParentChidren.Add(child2); + session.Save(grandChild2); + + var child3 = new Child + { + Name = "Jack", + Parent = parent2 + }; + parent2.Children.Add(child3); + child3.Parents.Add(parent2); + _child3Id = (Guid) session.Save(child3); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + } + + [TestFixture] + public class BagFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Parent); + rc.Bag(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("child_id")); + map.Inverse(true); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Child, x => x.Column("child_id")); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class SetFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Parent); + rc.Set(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("child_id")); + map.Inverse(true); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Child, x => x.Column("child_id")); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class ManyToManyBagFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => + { + map.Key(k => k.Column("Parent")); + map.Table("ParentChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Parents, + map => + { + map.Key(k => k.Column("Child")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ParentChild"); + }, + rel => rel.ManyToMany(map => map.Column("Parent"))); + rc.Bag(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("Child")); + map.Table("ChildGrandChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("GrandChild"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.ParentChidren, + map => + { + map.Key(k => k.Column("GrandChild")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ChildGrandChild"); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class ManyToManySetFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => + { + map.Key(k => k.Column("Parent")); + map.Table("ParentChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Parents, + map => + { + map.Key(k => k.Column("Child")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ParentChild"); + }, + rel => rel.ManyToMany(map => map.Column("Parent"))); + rc.Set(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("Child")); + map.Table("ChildGrandChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("GrandChild"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.ParentChidren, + map => + { + map.Key(k => k.Column("GrandChild")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ChildGrandChild"); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + // No bidi list/idbag: mapping not supported unless not marking inverse the parent side with list, which is not + // normal for a bidi. This is a general limitation, not a limitation of the feature tested here. + + [TestFixture] + public class UnidiBagFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiListFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Key(k => k.Column("child_id")); + map.Index(i => i.Column("i")); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + c => c.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Index(i => i.Column("i")); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiSetFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyBagFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyIdBagFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + c => c.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyListFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Key(k => k.Column("child_id")); + map.Index(i => i.Column("i")); + }, + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + c => c.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Index(i => i.Column("i")); + }, + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManySetFixtureAsync : FixtureBaseAsync + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs new file mode 100644 index 00000000000..e93c7d652f1 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs @@ -0,0 +1,299 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Collection; +using NHibernate.Engine.Query; +using NHibernate.Mapping.ByCode; +using NHibernate.Util; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.NH2319 +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class MapFixtureAsync : TestCaseMappingByCode + { + private Guid _parent1Id; + private Guid _child1Id; + private Guid _parent2Id; + private Guid _child3Id; + + [Test] + public Task ShouldBeAbleToFindChildrenByNameAsync() + { + return FindChildrenByNameAsync(_parent1Id, _child1Id); + } + + private async Task FindChildrenByNameAsync(Guid parentId, Guid childId, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(parentId, cancellationToken)); + + Assert.That(parent, Is.Not.Null); + + var filtered = await (parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync(cancellationToken)); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(filtered[0].Id, Is.EqualTo(childId)); + } + } + + [Test] + public async Task ShouldBeAbleToPerformComplexFilteringAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + + Assert.NotNull(parent); + + var filtered = await (parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Piter") + .SelectMany(x => x.GrandChildren) + .Select(x => x.Id) + .CountAsync()); + + Assert.That(filtered, Is.EqualTo(2)); + } + } + + [Test] + public async Task ShouldBeAbleToReuseQueryPlanAsync() + { + await (ShouldBeAbleToFindChildrenByNameAsync()); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + Assert.That(ShouldBeAbleToFindChildrenByNameAsync, Throws.Nothing); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public async Task ShouldNotMixResultsAsync() + { + await (FindChildrenByNameAsync(_parent1Id, _child1Id)); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + await (FindChildrenByNameAsync(_parent2Id, _child3Id)); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public async Task ShouldNotInitializeCollectionWhenPerformingQueryAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var persistentCollection = (IPersistentCollection) parent.ChildrenMap; + + var filtered = await (parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync()); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(persistentCollection.WasInitialized, Is.False); + } + } + + [Test] + public async Task ShouldPerformSqlQueryEvenIfCollectionAlreadyInitializedAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var loaded = parent.ChildrenMap.ToList(); + Assert.That(loaded, Has.Count.EqualTo(2)); + + var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + var filtered = await (parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToListAsync()); + + var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1)); + } + } + + [Test] + public async Task TestFilterAsync() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = await (session.GetAsync(_parent1Id)); + Assert.That(parent, Is.Not.Null); + + var children = await ((await (session.CreateFilterAsync(parent.ChildrenMap, "where this.Name = 'Jack'"))) + .ListAsync()); + + Assert.That(children, Has.Count.EqualTo(1)); + } + } + + [Test] + public async Task TestPlanCacheMissAsync() + { + var internalPlanCache = typeof(QueryPlanCache) + .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache; + Assert.That(internalPlanCache, Is.Not.Null, + $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation."); + + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + internalPlanCache.Clear(); + await (ShouldBeAbleToFindChildrenByNameAsync()); + AssertFilterPlanCacheMiss(spy); + } + } + + private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache"; + + private static void AssertFilterPlanCacheHit(LogSpy spy) => + // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed + // rather than only asserting it has been hit. + Assert.That(spy.GetWholeLog(), + Contains.Substring("located collection-filter query plan in cache (") + .And.Not.Contains(_filterPlanCacheMissLog)); + + private static void AssertFilterPlanCacheMiss(LogSpy spy) => + Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog)); + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("show_sql", "true"); + configuration.SetProperty("generate_statistics", "true"); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent1 = new Parent { Name = "Bob" }; + _parent1Id = (Guid) session.Save(parent1); + + var parent2 = new Parent { Name = "Martin" }; + _parent2Id = (Guid) session.Save(parent2); + + var child1 = new Child + { + Name = "Jack", + Parent = parent1 + }; + _child1Id = (Guid) session.Save(child1); + parent1.ChildrenMap.Add(child1.Id, child1); + + var child2 = new Child + { + Name = "Piter", + Parent = parent1 + }; + session.Save(child2); + parent1.ChildrenMap.Add(child2.Id, child2); + + var grandChild1 = new GrandChild + { + Name = "Kate", + Child = child2 + }; + session.Save(grandChild1); + child2.GrandChildren.Add(grandChild1); + + var grandChild2 = new GrandChild + { + Name = "Mary", + Child = child2 + }; + session.Save(grandChild2); + child2.GrandChildren.Add(grandChild2); + + var child3 = new Child + { + Name = "Jack", + Parent = parent2 + }; + _child3Id = (Guid) session.Save(child3); + parent2.ChildrenMap.Add(child1.Id, child3); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Map(x => x.ChildrenMap, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs b/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs index 5b5f1ac4b12..2ffc692763b 100644 --- a/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs @@ -61,6 +61,7 @@ public class TextLogSpy : IDisposable private readonly TextWriterAppender appender; private readonly Logger loggerImpl; private readonly StringBuilder stringBuilder; + private readonly Level previousLevel; public TextLogSpy(string loggerName, string pattern) { @@ -73,6 +74,7 @@ public TextLogSpy(string loggerName, string pattern) }; loggerImpl = (Logger)LogManager.GetLogger(typeof(LogsFixture).Assembly, loggerName).Logger; loggerImpl.AddAppender(appender); + previousLevel = loggerImpl.Level; loggerImpl.Level = Level.All; } @@ -87,9 +89,10 @@ public string[] Events public void Dispose() { loggerImpl.RemoveAppender(appender); + loggerImpl.Level = previousLevel; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs b/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs index d92cb3c9ef7..b9b5cca8398 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs @@ -20,9 +20,9 @@ public PersistentMilestoneCollection(ISessionImplementor session) : base(session public TValue FindValueFor(TKey key) { Read(); - return ((IMilestoneCollection) WrappedMap).FindValueFor(key); + return ((IMilestoneCollection) Entries(null)).FindValueFor(key); } #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs index b5c6910a8ce..7c4c618284a 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs @@ -39,8 +39,8 @@ public void LoadCompositeElementsWithWithSimpleHbmAliasInjection() Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithSimpleHbmAliasInjection"); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); CleanupWithPersons(); } @@ -51,8 +51,8 @@ public void LoadCompositeElementsWithWithComplexHbmAliasInjection() Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithComplexHbmAliasInjection"); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); CleanupWithPersons(); } @@ -63,8 +63,8 @@ public void LoadCompositeElementsWithWithCustomAliases() Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithCustomAliases"); Assert.That(country, Is.Not.Null); - Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys"); - Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements"); + Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys"); + Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements"); CleanupWithPersons(); } @@ -189,8 +189,8 @@ public void LoadCompositeElementCollectionWithCustomLoader() { var a = session.Get(country.Code); Assert.That(a, Is.Not.Null, "area"); - Assert.That((ICollection) a.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "area.Keys"); - Assert.That((ICollection) a.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "area.Elements"); + Assert.That(a.Statistics.Keys, Is.EquivalentTo(stats.Keys), "area.Keys"); + Assert.That(a.Statistics.Values, Is.EquivalentTo(stats.Values), "area.Elements"); } CleanupWithPersons(); } @@ -416,4 +416,4 @@ private static IDictionary CreateStatistics() #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs new file mode 100644 index 00000000000..0f83b28e91b --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs @@ -0,0 +1,728 @@ +using System; +using System.Linq; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Collection; +using NHibernate.Engine.Query; +using NHibernate.Mapping.ByCode; +using NHibernate.Util; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2319 +{ + [TestFixture] + public abstract class FixtureBase : TestCaseMappingByCode + { + private Guid _parent1Id; + private Guid _child1Id; + private Guid _parent2Id; + private Guid _child3Id; + + [Test] + public void ShouldBeAbleToFindChildrenByName() + { + FindChildrenByName(_parent1Id, _child1Id); + } + + private void FindChildrenByName(Guid parentId, Guid childId) + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(parentId); + + Assert.That(parent, Is.Not.Null); + + var filtered = parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(filtered[0].Id, Is.EqualTo(childId)); + } + } + + [Test] + public void ShouldBeAbleToPerformComplexFiltering() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + + Assert.NotNull(parent); + + var filtered = parent.Children + .AsQueryable() + .Where(x => x.Name == "Piter") + .SelectMany(x => x.GrandChildren) + .Select(x => x.Id) + .Count(); + + Assert.That(filtered, Is.EqualTo(2)); + } + } + + [Test] + public void ShouldBeAbleToReuseQueryPlan() + { + ShouldBeAbleToFindChildrenByName(); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + Assert.That(ShouldBeAbleToFindChildrenByName, Throws.Nothing); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public void ShouldNotMixResults() + { + FindChildrenByName(_parent1Id, _child1Id); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + FindChildrenByName(_parent2Id, _child3Id); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public void ShouldNotInitializeCollectionWhenPerformingQuery() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var persistentCollection = (IPersistentCollection) parent.Children; + + var filtered = parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(persistentCollection.WasInitialized, Is.False); + } + } + + [Test] + public void ShouldPerformSqlQueryEvenIfCollectionAlreadyInitialized() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var loaded = parent.Children.ToList(); + Assert.That(loaded, Has.Count.EqualTo(2)); + + var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + var filtered = parent.Children + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1)); + } + } + + [Test] + public void TestFilter() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var children = session.CreateFilter(parent.Children, "where this.Name = 'Jack'") + .List(); + + Assert.That(children, Has.Count.EqualTo(1)); + } + } + + [Test] + public void TestPlanCacheMiss() + { + var internalPlanCache = typeof(QueryPlanCache) + .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache; + Assert.That(internalPlanCache, Is.Not.Null, + $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation."); + + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + internalPlanCache.Clear(); + ShouldBeAbleToFindChildrenByName(); + AssertFilterPlanCacheMiss(spy); + } + } + + private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache"; + + private static void AssertFilterPlanCacheHit(LogSpy spy) => + // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed + // rather than only asserting it has been hit. + Assert.That(spy.GetWholeLog(), + Contains.Substring("located collection-filter query plan in cache (") + .And.Not.Contains(_filterPlanCacheMissLog)); + + private static void AssertFilterPlanCacheMiss(LogSpy spy) => + Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog)); + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("show_sql", "true"); + configuration.SetProperty("generate_statistics", "true"); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent1 = new Parent { Name = "Bob" }; + _parent1Id = (Guid) session.Save(parent1); + + var parent2 = new Parent { Name = "Martin" }; + _parent2Id = (Guid) session.Save(parent2); + + var child1 = new Child + { + Name = "Jack", + Parent = parent1 + }; + parent1.Children.Add(child1); + child1.Parents.Add(parent1); + _child1Id = (Guid) session.Save(child1); + + var child2 = new Child + { + Name = "Piter", + Parent = parent1 + }; + parent1.Children.Add(child2); + child2.Parents.Add(parent1); + session.Save(child2); + + var grandChild1 = new GrandChild + { + Name = "Kate", + Child = child2 + }; + child2.GrandChildren.Add(grandChild1); + grandChild1.ParentChidren.Add(child2); + session.Save(grandChild1); + + var grandChild2 = new GrandChild + { + Name = "Mary", + Child = child2 + }; + child2.GrandChildren.Add(grandChild2); + grandChild2.ParentChidren.Add(child2); + session.Save(grandChild2); + + var child3 = new Child + { + Name = "Jack", + Parent = parent2 + }; + parent2.Children.Add(child3); + child3.Parents.Add(parent2); + _child3Id = (Guid) session.Save(child3); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + } + + [TestFixture] + public class BagFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Parent); + rc.Bag(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("child_id")); + map.Inverse(true); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Child, x => x.Column("child_id")); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class SetFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Parent); + rc.Set(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("child_id")); + map.Inverse(true); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Child, x => x.Column("child_id")); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class ManyToManyBagFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => + { + map.Key(k => k.Column("Parent")); + map.Table("ParentChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Parents, + map => + { + map.Key(k => k.Column("Child")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ParentChild"); + }, + rel => rel.ManyToMany(map => map.Column("Parent"))); + rc.Bag(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("Child")); + map.Table("ChildGrandChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("GrandChild"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.ParentChidren, + map => + { + map.Key(k => k.Column("GrandChild")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ChildGrandChild"); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class ManyToManySetFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => + { + map.Key(k => k.Column("Parent")); + map.Table("ParentChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Parents, + map => + { + map.Key(k => k.Column("Child")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ParentChild"); + }, + rel => rel.ManyToMany(map => map.Column("Parent"))); + rc.Set(x => x.GrandChildren, + map => + { + map.Key(k => k.Column("Child")); + map.Table("ChildGrandChild"); + map.Inverse(true); + }, + rel => rel.ManyToMany(map => map.Column("GrandChild"))); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.ParentChidren, + map => + { + map.Key(k => k.Column("GrandChild")); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Table("ChildGrandChild"); + }, + rel => rel.ManyToMany(map => map.Column("Child"))); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + // No bidi list/idbag: mapping not supported unless not marking inverse the parent side with list, which is not + // normal for a bidi. This is a general limitation, not a limitation of the feature tested here. + + [TestFixture] + public class UnidiBagFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiListFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Key(k => k.Column("child_id")); + map.Index(i => i.Column("i")); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + c => c.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Index(i => i.Column("i")); + }, + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiSetFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyBagFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Bag(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyIdBagFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + c => c.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManyListFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + x => x.Children, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Key(k => k.Column("child_id")); + map.Index(i => i.Column("i")); + }, + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.List( + c => c.GrandChildren, + map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Index(i => i.Column("i")); + }, + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } + + [TestFixture] + public class UnidiManyToManySetFixture : FixtureBase + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.Children, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.ManyToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs new file mode 100644 index 00000000000..6aa9b1c8e86 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs @@ -0,0 +1,286 @@ +using System; +using System.Linq; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Collection; +using NHibernate.Engine.Query; +using NHibernate.Mapping.ByCode; +using NHibernate.Util; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2319 +{ + [TestFixture] + public class MapFixture : TestCaseMappingByCode + { + private Guid _parent1Id; + private Guid _child1Id; + private Guid _parent2Id; + private Guid _child3Id; + + [Test] + public void ShouldBeAbleToFindChildrenByName() + { + FindChildrenByName(_parent1Id, _child1Id); + } + + private void FindChildrenByName(Guid parentId, Guid childId) + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(parentId); + + Assert.That(parent, Is.Not.Null); + + var filtered = parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(filtered[0].Id, Is.EqualTo(childId)); + } + } + + [Test] + public void ShouldBeAbleToPerformComplexFiltering() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + + Assert.NotNull(parent); + + var filtered = parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Piter") + .SelectMany(x => x.GrandChildren) + .Select(x => x.Id) + .Count(); + + Assert.That(filtered, Is.EqualTo(2)); + } + } + + [Test] + public void ShouldBeAbleToReuseQueryPlan() + { + ShouldBeAbleToFindChildrenByName(); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + Assert.That(ShouldBeAbleToFindChildrenByName, Throws.Nothing); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public void ShouldNotMixResults() + { + FindChildrenByName(_parent1Id, _child1Id); + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + FindChildrenByName(_parent2Id, _child3Id); + AssertFilterPlanCacheHit(spy); + } + } + + [Test] + public void ShouldNotInitializeCollectionWhenPerformingQuery() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var persistentCollection = (IPersistentCollection) parent.ChildrenMap; + + var filtered = parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(persistentCollection.WasInitialized, Is.False); + } + } + + [Test] + public void ShouldPerformSqlQueryEvenIfCollectionAlreadyInitialized() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var loaded = parent.ChildrenMap.ToList(); + Assert.That(loaded, Has.Count.EqualTo(2)); + + var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + var filtered = parent.ChildrenMap.Values + .AsQueryable() + .Where(x => x.Name == "Jack") + .ToList(); + + var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount; + + Assert.That(filtered, Has.Count.EqualTo(1)); + Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1)); + } + } + + [Test] + public void TestFilter() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var parent = session.Get(_parent1Id); + Assert.That(parent, Is.Not.Null); + + var children = session.CreateFilter(parent.ChildrenMap, "where this.Name = 'Jack'") + .List(); + + Assert.That(children, Has.Count.EqualTo(1)); + } + } + + [Test] + public void TestPlanCacheMiss() + { + var internalPlanCache = typeof(QueryPlanCache) + .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache; + Assert.That(internalPlanCache, Is.Not.Null, + $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation."); + + using (var spy = new LogSpy(typeof(QueryPlanCache))) + { + internalPlanCache.Clear(); + ShouldBeAbleToFindChildrenByName(); + AssertFilterPlanCacheMiss(spy); + } + } + + private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache"; + + private static void AssertFilterPlanCacheHit(LogSpy spy) => + // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed + // rather than only asserting it has been hit. + Assert.That(spy.GetWholeLog(), + Contains.Substring("located collection-filter query plan in cache (") + .And.Not.Contains(_filterPlanCacheMissLog)); + + private static void AssertFilterPlanCacheMiss(LogSpy spy) => + Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog)); + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("show_sql", "true"); + configuration.SetProperty("generate_statistics", "true"); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent1 = new Parent { Name = "Bob" }; + _parent1Id = (Guid) session.Save(parent1); + + var parent2 = new Parent { Name = "Martin" }; + _parent2Id = (Guid) session.Save(parent2); + + var child1 = new Child + { + Name = "Jack", + Parent = parent1 + }; + _child1Id = (Guid) session.Save(child1); + parent1.ChildrenMap.Add(child1.Id, child1); + + var child2 = new Child + { + Name = "Piter", + Parent = parent1 + }; + session.Save(child2); + parent1.ChildrenMap.Add(child2.Id, child2); + + var grandChild1 = new GrandChild + { + Name = "Kate", + Child = child2 + }; + session.Save(grandChild1); + child2.GrandChildren.Add(grandChild1); + + var grandChild2 = new GrandChild + { + Name = "Mary", + Child = child2 + }; + session.Save(grandChild2); + child2.GrandChildren.Add(grandChild2); + + var child3 = new Child + { + Name = "Jack", + Parent = parent2 + }; + _child3Id = (Guid) session.Save(child3); + parent2.ChildrenMap.Add(child1.Id, child3); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Map(x => x.ChildrenMap, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Set(x => x.GrandChildren, + map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans), + rel => rel.OneToMany()); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs new file mode 100644 index 00000000000..ba5ef9b1368 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH2319 +{ + class Parent + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual ICollection Children { get; set; } = new List(); + public virtual IDictionary ChildrenMap { get; set; } = new Dictionary(); + } + + class Child + { + public virtual Parent Parent { get; set; } + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual ICollection GrandChildren { get; set; } = new List(); + public virtual ICollection Parents { get; set; } = new List(); + } + class GrandChild + { + public virtual Child Child { get; set; } + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual ICollection ParentChidren { get; set; } = new List(); + } +} diff --git a/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs b/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs index 4852e0315aa..bf0f8338457 100644 --- a/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs +++ b/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs @@ -684,7 +684,6 @@ void Clone_TransactionCompleted(object sender, TransactionEventArgs e) [OneTimeSetUp] public void TestFixtureSetUp() { - ((Logger)_log.Logger).Level = log4net.Core.Level.Info; _spy = new LogSpy(_log); _spy.Appender.Layout = new PatternLayout("%d{ABSOLUTE} [%t] - %m%n"); } diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs index f681e4940b9..4275faabc27 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs @@ -13,8 +13,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -27,7 +30,7 @@ namespace NHibernate.Collection.Generic /// /// Contains generated async methods /// - public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList + public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList, IQueryable { public override async Task DisassembleAsync(ICollectionPersister persister, CancellationToken cancellationToken) diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs index b56e346783e..f2a3ed3bcd5 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -14,9 +14,11 @@ using System.Data.Common; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Id; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -28,7 +30,7 @@ namespace NHibernate.Collection.Generic /// /// Contains generated async methods /// - public partial class PersistentIdentifierBag : AbstractPersistentCollection, IList, IList + public partial class PersistentIdentifierBag : AbstractPersistentCollection, IList, IList, IQueryable { /// @@ -200,4 +202,4 @@ public override async Task PreInsertAsync(ICollectionPersister persister, Cancel } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs index f8de6b71f80..04c658fcdeb 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs @@ -13,8 +13,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -27,7 +30,7 @@ namespace NHibernate.Collection.Generic /// /// Contains generated async methods /// - public partial class PersistentGenericList : AbstractPersistentCollection, IList, IList + public partial class PersistentGenericList : AbstractPersistentCollection, IList, IList, IQueryable { public override Task GetOrphansAsync(object snapshot, string entityName, CancellationToken cancellationToken) diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs index bda6cc9bcfc..ad77b5d50dc 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs @@ -13,8 +13,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -154,4 +157,4 @@ public override async Task NeedsUpdatingAsync(object entry, int i, IType e || (!isNew && ((e.Value == null) != (snValue == null))); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs index 9f381dd659f..96138b1b7e3 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs @@ -14,9 +14,11 @@ using System.Data.Common; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -29,7 +31,7 @@ namespace NHibernate.Collection.Generic /// /// Contains generated async methods /// - public partial class PersistentGenericSet : AbstractPersistentCollection, ISet + public partial class PersistentGenericSet : AbstractPersistentCollection, ISet, IQueryable { public override Task GetOrphansAsync(object snapshot, string entityName, CancellationToken cancellationToken) diff --git a/src/NHibernate/Async/Engine/ISessionImplementor.cs b/src/NHibernate/Async/Engine/ISessionImplementor.cs index d24e0437948..f294f5b27af 100644 --- a/src/NHibernate/Async/Engine/ISessionImplementor.cs +++ b/src/NHibernate/Async/Engine/ISessionImplementor.cs @@ -111,6 +111,11 @@ public partial interface ISessionImplementor /// Task ListFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken); + /// + /// Execute a filter + /// + Task ListFilterAsync(object collection, IQueryExpression queryExpression, QueryParameters parameters, CancellationToken cancellationToken); + /// /// Execute a filter (strongly-typed version). /// @@ -176,5 +181,7 @@ public partial interface ISessionImplementor /// Execute a HQL update or delete query Task ExecuteUpdateAsync(IQueryExpression query, QueryParameters queryParameters, CancellationToken cancellationToken); + + Task CreateFilterAsync(object collection, IQueryExpression queryExpression, CancellationToken cancellationToken); } } diff --git a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs index c033e918250..9ab156012d1 100644 --- a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs @@ -92,6 +92,18 @@ public virtual async Task ListAsync(CriteriaImpl criteria, CancellationTo } public abstract Task ListFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken); + public async Task ListFilterAsync(object collection, IQueryExpression queryExpression, QueryParameters parameters, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var results = (IList)typeof(List<>).MakeGenericType(queryExpression.Type) + .GetConstructor(System.Type.EmptyTypes) + .Invoke(null); + + await (ListFilterAsync(collection, queryExpression, parameters, results, cancellationToken)).ConfigureAwait(false); + return results; + } + protected abstract Task ListFilterAsync(object collection, IQueryExpression queryExpression, QueryParameters parameters, IList results, CancellationToken cancellationToken); + public abstract Task> ListFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken); public abstract Task EnumerableFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken); public abstract Task> EnumerableFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken); @@ -170,6 +182,8 @@ protected async Task AfterOperationAsync(bool success, CancellationToken cancell } } + public abstract Task CreateFilterAsync(object collection, IQueryExpression queryExpression, CancellationToken cancellationToken); + public abstract Task EnumerableAsync(IQueryExpression queryExpression, QueryParameters queryParameters, CancellationToken cancellationToken); public abstract Task> EnumerableAsync(IQueryExpression queryExpression, QueryParameters queryParameters, CancellationToken cancellationToken); diff --git a/src/NHibernate/Async/Impl/ExpressionQueryImpl.cs b/src/NHibernate/Async/Impl/ExpressionQueryImpl.cs new file mode 100644 index 00000000000..91e5c6b04c4 --- /dev/null +++ b/src/NHibernate/Async/Impl/ExpressionQueryImpl.cs @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +// +// 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.Text; +using NHibernate.Engine; +using NHibernate.Engine.Query; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Hql.Ast.ANTLR.Tree; +using NHibernate.Hql.Ast.ANTLR.Util; +using NHibernate.Type; +using NHibernate.Util; + +namespace NHibernate.Impl +{ + using System.Threading.Tasks; + using System.Threading; + + /// + /// Contains generated async methods + /// + partial class ExpressionFilterImpl : ExpressionQueryImpl + { + + public override async Task ListAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + VerifyParameters(); + var namedParams = NamedParams; + Before(); + try + { + return await (Session.ListFilterAsync(collection, ExpandParameters(namedParams), GetQueryParameters(namedParams), cancellationToken)).ConfigureAwait(false); + } + finally + { + After(); + } + } + } +} diff --git a/src/NHibernate/Async/Impl/SessionImpl.cs b/src/NHibernate/Async/Impl/SessionImpl.cs index 0155eb0b253..be929645120 100644 --- a/src/NHibernate/Async/Impl/SessionImpl.cs +++ b/src/NHibernate/Async/Impl/SessionImpl.cs @@ -228,15 +228,42 @@ async Task FindAsync(string query, object[] values, IType[] types, Cancel } } - public override async Task ListAsync(IQueryExpression queryExpression, QueryParameters queryParameters, IList results, CancellationToken cancellationToken) + public override Task ListAsync(IQueryExpression queryExpression, QueryParameters queryParameters, IList results, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return ListAsync(queryExpression, queryParameters, results, null, cancellationToken); + } + + protected override Task ListFilterAsync(object collection, IQueryExpression queryExpression, QueryParameters queryParameters, IList results, CancellationToken cancellationToken) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return ListAsync(queryExpression, queryParameters, results, collection, cancellationToken); + } + + private async Task ListAsync(IQueryExpression queryExpression, QueryParameters queryParameters, IList results, object filterConnection, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); queryParameters.ValidateParameters(); - var plan = GetHQLQueryPlan(queryExpression, false); - await (AutoFlushIfRequiredAsync(plan.QuerySpaces, cancellationToken)).ConfigureAwait(false); + + var isFilter = filterConnection != null; + var plan = isFilter + ? await (GetFilterQueryPlanAsync(filterConnection, queryExpression, queryParameters, false, cancellationToken)).ConfigureAwait(false) + : GetHQLQueryPlan(queryExpression, false); + + // GetFilterQueryPlan has already auto flushed or fully flush. + if (!isFilter) + await (AutoFlushIfRequiredAsync(plan.QuerySpaces, cancellationToken)).ConfigureAwait(false); bool success = false; using (SuspendAutoFlush()) //stops flush being called multiple times if this method is recursively called @@ -377,13 +404,6 @@ public override async Task EnumerableAsync(IQueryExpression queryEx } } - /// - /// - /// - /// - /// - /// A cancellation token that can be used to cancel the work - /// public async Task CreateFilterAsync(object collection, string queryString, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -391,59 +411,92 @@ public override async Task EnumerableAsync(IQueryExpression queryEx { CheckAndUpdateSessionStatus(); - CollectionFilterImpl filter = - new CollectionFilterImpl(queryString, collection, this, - (await (GetFilterQueryPlanAsync(collection, queryString, null, false, cancellationToken)).ConfigureAwait(false)).ParameterMetadata); + var plan = await (GetFilterQueryPlanAsync(collection, queryString, null, false, cancellationToken)).ConfigureAwait(false); + var filter = new CollectionFilterImpl(queryString, collection, this, plan.ParameterMetadata); //filter.SetComment(queryString); return filter; } } - private async Task GetFilterQueryPlanAsync(object collection, string filter, QueryParameters parameters, bool shallow, CancellationToken cancellationToken) + public override async Task CreateFilterAsync(object collection, IQueryExpression queryExpression, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + using (new SessionIdLoggingContext(SessionId)) + { + CheckAndUpdateSessionStatus(); + + var plan = await (GetFilterQueryPlanAsync(collection, queryExpression, null, false, cancellationToken)).ConfigureAwait(false); + var filter = new ExpressionFilterImpl(plan.QueryExpression, collection, this, plan.ParameterMetadata); + return filter; + } + } + + private Task GetFilterQueryPlanAsync(object collection, IQueryExpression queryExpression, QueryParameters parameters, bool shallow, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return GetFilterQueryPlanAsync(collection, parameters, shallow, null, queryExpression, cancellationToken); + } + + private Task GetFilterQueryPlanAsync(object collection, string filter, QueryParameters parameters, bool shallow, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return GetFilterQueryPlanAsync(collection, parameters, shallow, filter, null, cancellationToken); + } + + private async Task GetFilterQueryPlanAsync(object collection, QueryParameters parameters, bool shallow, + string filter, IQueryExpression queryExpression, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (new SessionIdLoggingContext(SessionId)) { if (collection == null) - { - throw new ArgumentNullException("collection", "null collection passed to filter"); - } + throw new ArgumentNullException(nameof(collection), "null collection passed to filter"); + if (filter != null && queryExpression != null) + throw new ArgumentException($"Either {nameof(filter)} or {nameof(queryExpression)} must be specified, not both."); + if (filter == null && queryExpression == null) + throw new ArgumentException($"{nameof(filter)} and {nameof(queryExpression)} were both null."); - CollectionEntry entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleBeforeFlush = (entry == null) ? null : entry.LoadedPersister; + var entry = persistenceContext.GetCollectionEntryOrNull(collection); + var roleBeforeFlush = entry?.LoadedPersister; - FilterQueryPlan plan; + IQueryExpressionPlan plan; if (roleBeforeFlush == null) { // if it was previously unreferenced, we need to flush in order to // get its state into the database in order to execute query await (FlushAsync(cancellationToken)).ConfigureAwait(false); entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleAfterFlush = (entry == null) ? null : entry.LoadedPersister; + var roleAfterFlush = entry?.LoadedPersister; if (roleAfterFlush == null) { throw new QueryException("The collection was unreferenced"); } - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleAfterFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleAfterFlush.Role, shallow, filter, queryExpression); } else { // otherwise, we only need to flush if there are in-memory changes // to the queried tables - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleBeforeFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleBeforeFlush.Role, shallow, filter, queryExpression); if (await (AutoFlushIfRequiredAsync(plan.QuerySpaces, cancellationToken)).ConfigureAwait(false)) { // might need to run a different filter entirely after the flush // because the collection role may have changed entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleAfterFlush = (entry == null) ? null : entry.LoadedPersister; + var roleAfterFlush = entry?.LoadedPersister; if (roleBeforeFlush != roleAfterFlush) { if (roleAfterFlush == null) { throw new QueryException("The collection was dereferenced"); } - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleAfterFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleAfterFlush.Role, shallow, filter, queryExpression); } } } @@ -1021,7 +1074,7 @@ private async Task FilterAsync(object collection, string filter, QueryParameters using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, false, cancellationToken)).ConfigureAwait(false); + var plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, false, cancellationToken)).ConfigureAwait(false); bool success = false; using (SuspendAutoFlush()) //stops flush being called multiple times if this method is recursively called @@ -1076,7 +1129,7 @@ public override async Task EnumerableFilterAsync(object collection, using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, true, cancellationToken)).ConfigureAwait(false); + var plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, true, cancellationToken)).ConfigureAwait(false); return await (plan.PerformIterateAsync(queryParameters, this, cancellationToken)).ConfigureAwait(false); } } @@ -1087,7 +1140,7 @@ public override async Task> EnumerableFilterAsync(object colle using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, true, cancellationToken)).ConfigureAwait(false); + var plan = await (GetFilterQueryPlanAsync(collection, filter, queryParameters, true, cancellationToken)).ConfigureAwait(false); return await (plan.PerformIterateAsync(queryParameters, this, cancellationToken)).ConfigureAwait(false); } } diff --git a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs index 3862392cb28..a5c8a51f222 100644 --- a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs @@ -92,6 +92,11 @@ public override Task ImmediateLoadAsync(string entityName, object id, Ca throw new SessionException("proxies cannot be fetched by a stateless session"); } + public override Task CreateFilterAsync(object collection, IQueryExpression queryExpression, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + public override async Task ListAsync(IQueryExpression queryExpression, QueryParameters queryParameters, IList results, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -181,6 +186,11 @@ public override Task ListFilterAsync(object collection, string filter, Qu throw new NotSupportedException(); } + protected override Task ListFilterAsync(object collection, IQueryExpression queryExpression, QueryParameters parameters, IList results, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + public override Task> ListFilterAsync(object collection, string filter, QueryParameters parameters, CancellationToken cancellationToken) { throw new NotSupportedException(); diff --git a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs index f8fec50608c..51dbeaf98b9 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -22,7 +25,7 @@ namespace NHibernate.Collection.Generic /// The underlying collection used is an [Serializable] [DebuggerTypeProxy(typeof (CollectionProxy<>))] - public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList + public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList, IQueryable { // TODO NH: find a way to writeonce (no duplicated code from PersistentBag) @@ -504,6 +507,21 @@ public override string ToString() return StringHelper.CollectionToString(_gbag); } + #region IQueryable Members + + [NonSerialized] + IQueryable _queryable; + + Expression IQueryable.Expression => InnerQueryable.Expression; + + System.Type IQueryable.ElementType => InnerQueryable.ElementType; + + IQueryProvider IQueryable.Provider => InnerQueryable.Provider; + + IQueryable InnerQueryable => _queryable ?? (_queryable = new NhQueryable(Session, this)); + + #endregion + /// /// Counts the number of times that the occurs /// in the . diff --git a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs index 774882ee9d2..85bb25d93c5 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -4,9 +4,11 @@ using System.Data.Common; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Id; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -30,7 +32,7 @@ namespace NHibernate.Collection.Generic /// [Serializable] [DebuggerTypeProxy(typeof (CollectionProxy<>))] - public partial class PersistentIdentifierBag : AbstractPersistentCollection, IList, IList + public partial class PersistentIdentifierBag : AbstractPersistentCollection, IList, IList, IQueryable { /* NH considerations: * For various reason we know that the underlining type will be a List or a @@ -42,7 +44,7 @@ public partial class PersistentIdentifierBag : AbstractPersistentCollection, private Dictionary _identifiers; //index -> id private IList _values; //element - + public PersistentIdentifierBag() {} public PersistentIdentifierBag(ISessionImplementor session) : base(session) {} @@ -514,5 +516,20 @@ public override int GetHashCode() return (Id != null ? Id.GetHashCode() : 0); } } + + #region IQueryable Members + + [NonSerialized] + IQueryable _queryable; + + Expression IQueryable.Expression => InnerQueryable.Expression; + + System.Type IQueryable.ElementType => InnerQueryable.ElementType; + + IQueryProvider IQueryable.Provider => InnerQueryable.Provider; + + IQueryable InnerQueryable => _queryable ?? (_queryable = new NhQueryable(Session, this)); + + #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Collection/Generic/PersistentGenericList.cs index 15631e9fb68..e913a9d539d 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericList.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -19,7 +22,7 @@ namespace NHibernate.Collection.Generic /// The underlying collection used is a [Serializable] [DebuggerTypeProxy(typeof (CollectionProxy<>))] - public partial class PersistentGenericList : AbstractPersistentCollection, IList, IList + public partial class PersistentGenericList : AbstractPersistentCollection, IList, IList, IQueryable { protected IList WrappedList; @@ -497,7 +500,6 @@ IEnumerator IEnumerable.GetEnumerator() #endregion - #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() @@ -508,6 +510,20 @@ IEnumerator IEnumerable.GetEnumerator() #endregion + #region IQueryable Members + + [NonSerialized] + IQueryable _queryable; + + Expression IQueryable.Expression => InnerQueryable.Expression; + + System.Type IQueryable.ElementType => InnerQueryable.ElementType; + + IQueryProvider IQueryable.Provider => InnerQueryable.Provider; + + IQueryable InnerQueryable => _queryable ?? (_queryable = new NhQueryable(Session, this)); + + #endregion #region DelayedOperations diff --git a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs index 903f9260f17..1f34cd7f06a 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -23,14 +26,21 @@ namespace NHibernate.Collection.Generic public partial class PersistentGenericMap : AbstractPersistentCollection, IDictionary, ICollection { protected IDictionary WrappedMap; + private readonly ICollection _wrappedValues; - public PersistentGenericMap() { } + public PersistentGenericMap() + { + _wrappedValues = new ValuesWrapper(this); + } /// /// Construct an uninitialized PersistentGenericMap. /// /// The ISession the PersistentGenericMap should be a part of. - public PersistentGenericMap(ISessionImplementor session) : base(session) { } + public PersistentGenericMap(ISessionImplementor session) : base(session) + { + _wrappedValues = new ValuesWrapper(this); + } /// /// Construct an initialized PersistentGenericMap based off the values from the existing IDictionary. @@ -41,6 +51,7 @@ public PersistentGenericMap(ISessionImplementor session, IDictionary Values { get { - Read(); - return WrappedMap.Values; + return _wrappedValues; } } @@ -568,5 +578,85 @@ public void Operate() } #endregion + + [Serializable] + private class ValuesWrapper : ICollection, IQueryable + { + private readonly PersistentGenericMap _map; + + public ValuesWrapper(PersistentGenericMap map) + { + _map = map; + } + + #region IQueryable Members + + [NonSerialized] + private IQueryable _queryable; + + Expression IQueryable.Expression => InnerQueryable.Expression; + + System.Type IQueryable.ElementType => InnerQueryable.ElementType; + + IQueryProvider IQueryable.Provider => InnerQueryable.Provider; + + private IQueryable InnerQueryable => _queryable ?? (_queryable = new NhQueryable(_map.Session, _map)); + + #endregion + + #region ICollection Members + + public IEnumerator GetEnumerator() + { + _map.Read(); + return _map.WrappedMap.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + _map.Read(); + return GetEnumerator(); + } + + public void Add(TValue item) + { + throw new NotSupportedException("Values collection is readonly"); + } + + public void Clear() + { + throw new NotSupportedException("Values collection is readonly"); + } + + public bool Contains(TValue item) + { + _map.Read(); + return _map.WrappedMap.Values.Contains(item); + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + _map.Read(); + _map.WrappedMap.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(TValue item) + { + throw new NotSupportedException("Values collection is readonly"); + } + + public int Count + { + get + { + _map.Read(); + return _map.WrappedMap.Values.Count; + } + } + + public bool IsReadOnly => true; + + #endregion + } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs index bf25259c16d..f7b6110f578 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs @@ -4,9 +4,11 @@ using System.Data.Common; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; using NHibernate.DebugHelpers; using NHibernate.Engine; +using NHibernate.Linq; using NHibernate.Loader; using NHibernate.Persister.Collection; using NHibernate.Type; @@ -19,7 +21,7 @@ namespace NHibernate.Collection.Generic /// [Serializable] [DebuggerTypeProxy(typeof(CollectionProxy<>))] - public partial class PersistentGenericSet : AbstractPersistentCollection, ISet + public partial class PersistentGenericSet : AbstractPersistentCollection, ISet, IQueryable { /// /// The that NHibernate is wrapping. @@ -492,7 +494,6 @@ public bool IsSynchronized get { return false; } } - void ICollection.Add(T item) { Add(item); @@ -520,6 +521,20 @@ public IEnumerator GetEnumerator() #endregion + #region IQueryable Members + + [NonSerialized] + IQueryable _queryable; + + Expression IQueryable.Expression => InnerQueryable.Expression; + + System.Type IQueryable.ElementType => InnerQueryable.ElementType; + + IQueryProvider IQueryable.Provider => InnerQueryable.Provider; + + IQueryable InnerQueryable => _queryable ?? (_queryable = new NhQueryable(Session, this)); + + #endregion #region DelayedOperations diff --git a/src/NHibernate/Engine/ISessionImplementor.cs b/src/NHibernate/Engine/ISessionImplementor.cs index 86c2e5945eb..934e8669d20 100644 --- a/src/NHibernate/Engine/ISessionImplementor.cs +++ b/src/NHibernate/Engine/ISessionImplementor.cs @@ -123,6 +123,11 @@ public partial interface ISessionImplementor /// IList ListFilter(object collection, string filter, QueryParameters parameters); + /// + /// Execute a filter + /// + IList ListFilter(object collection, IQueryExpression queryExpression, QueryParameters parameters); + /// /// Execute a filter (strongly-typed version). /// @@ -317,6 +322,8 @@ public partial interface ISessionImplementor void CloseSessionFromSystemTransaction(); + IQuery CreateFilter(object collection, IQueryExpression queryExpression); + EntityKey GenerateEntityKey(object id, IEntityPersister persister); CacheKey GenerateCacheKey(object id, IType type, string entityOrRoleName); diff --git a/src/NHibernate/Engine/Query/FilterQueryPlan.cs b/src/NHibernate/Engine/Query/FilterQueryPlan.cs index 33faa73eee1..41ed7769443 100644 --- a/src/NHibernate/Engine/Query/FilterQueryPlan.cs +++ b/src/NHibernate/Engine/Query/FilterQueryPlan.cs @@ -10,17 +10,23 @@ namespace NHibernate.Engine.Query [Serializable] public class FilterQueryPlan : QueryExpressionPlan { - private readonly string collectionRole; - public FilterQueryPlan(IQueryExpression queryExpression, string collectionRole, bool shallow, IDictionary enabledFilters, ISessionFactoryImplementor factory) - : base(queryExpression.Key, CreateTranslators(queryExpression, collectionRole, shallow, enabledFilters, factory)) + : base(queryExpression, collectionRole, shallow, enabledFilters, factory) { - this.collectionRole = collectionRole; + CollectionRole = collectionRole; } - public string CollectionRole + protected FilterQueryPlan(FilterQueryPlan source, IQueryExpression expression) + : base (source, expression) + { + CollectionRole = source.CollectionRole; + } + + public string CollectionRole { get; } + + public override QueryExpressionPlan Copy(IQueryExpression expression) { - get { return collectionRole; } + return new FilterQueryPlan(this, expression); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Engine/Query/QueryExpressionPlan.cs b/src/NHibernate/Engine/Query/QueryExpressionPlan.cs index d2ff2cef1bd..0a1184a121f 100644 --- a/src/NHibernate/Engine/Query/QueryExpressionPlan.cs +++ b/src/NHibernate/Engine/Query/QueryExpressionPlan.cs @@ -8,7 +8,13 @@ namespace NHibernate.Engine.Query [Serializable] public class QueryExpressionPlan : HQLQueryPlan, IQueryExpressionPlan { - public IQueryExpression QueryExpression { get; private set; } + public IQueryExpression QueryExpression { get; } + + public QueryExpressionPlan(IQueryExpression queryExpression, string collectionRole, bool shallow, IDictionary enabledFilters, ISessionFactoryImplementor factory) + : this(queryExpression.Key, CreateTranslators(queryExpression, collectionRole, shallow, enabledFilters, factory)) + { + QueryExpression = queryExpression; + } public QueryExpressionPlan(IQueryExpression queryExpression, bool shallow, IDictionary enabledFilters, ISessionFactoryImplementor factory) : this(queryExpression.Key, CreateTranslators(queryExpression, null, shallow, enabledFilters, factory)) @@ -21,7 +27,7 @@ protected QueryExpressionPlan(string key, IQueryTranslator[] translators) { } - private QueryExpressionPlan(HQLQueryPlan source, IQueryExpression expression) + protected QueryExpressionPlan(HQLQueryPlan source, IQueryExpression expression) : base(source) { QueryExpression = expression; @@ -32,9 +38,9 @@ protected static IQueryTranslator[] CreateTranslators(IQueryExpression queryExpr return factory.Settings.QueryTranslatorFactory.CreateQueryTranslators(queryExpression, collectionRole, shallow, enabledFilters, factory); } - public QueryExpressionPlan Copy(IQueryExpression expression) + public virtual QueryExpressionPlan Copy(IQueryExpression expression) { return new QueryExpressionPlan(this, expression); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Engine/Query/QueryPlanCache.cs b/src/NHibernate/Engine/Query/QueryPlanCache.cs index b62021874fe..a7083916f45 100644 --- a/src/NHibernate/Engine/Query/QueryPlanCache.cs +++ b/src/NHibernate/Engine/Query/QueryPlanCache.cs @@ -67,49 +67,55 @@ public IQueryExpressionPlan GetHQLQueryPlan(IQueryExpression queryExpression, bo { log.Debug("located HQL query plan in cache (" + queryExpression.Key + ")"); } - var planExpression = plan.QueryExpression as NhLinqExpression; - var expression = queryExpression as NhLinqExpression; - if (planExpression != null && expression != null) - { - //NH-3413 - //Here we have to use original expression. - //In most cases NH do not translate expression in second time, but - // for cases when we have list parameters in query, like @p1.Contains(...), - // it does, and then it uses parameters from first try. - //TODO: cache only required parts of QueryExpression - - //NH-3436 - // We have to return new instance plan with it's own query expression - // because other treads can override queryexpression of current plan during execution of query if we will use cached instance of plan - expression.CopyExpressionTranslation(planExpression); - plan = plan.Copy(expression); - } + plan = CopyIfRequired(plan, queryExpression); + } + + return plan; + } + + private static QueryExpressionPlan CopyIfRequired(QueryExpressionPlan plan, IQueryExpression queryExpression) + { + var planExpression = plan.QueryExpression as NhLinqExpression; + var expression = queryExpression as NhLinqExpression; + if (planExpression != null && expression != null) + { + //NH-3413 + //Here we have to use original expression. + //In most cases NH do not translate expression in second time, but + // for cases when we have list parameters in query, like @p1.Contains(...), + // it does, and then it uses parameters from first try. + //TODO: cache only required parts of QueryExpression + + //NH-3436 + // We have to return new instance plan with it's own query expression + // because other treads can override queryexpression of current plan during execution of query if we will use cached instance of plan + expression.CopyExpressionTranslation(planExpression); + plan = plan.Copy(expression); } return plan; } - public FilterQueryPlan GetFilterQueryPlan(string filterString, string collectionRole, bool shallow, IDictionary enabledFilters) + public IQueryExpressionPlan GetFilterQueryPlan(string filterString, string collectionRole, bool shallow, IDictionary enabledFilters) { - var key = new FilterQueryPlanKey(filterString, collectionRole, shallow, enabledFilters); - var plan = (FilterQueryPlan) planCache[key]; + return GetFilterQueryPlan(new StringQueryExpression(filterString), collectionRole, shallow, enabledFilters); + } + + public IQueryExpressionPlan GetFilterQueryPlan(IQueryExpression queryExpression, string collectionRole, bool shallow, IDictionary enabledFilters) + { + var key = new FilterQueryPlanKey(queryExpression.Key, collectionRole, shallow, enabledFilters); + var plan = (QueryExpressionPlan) planCache[key]; if (plan == null) { - if (log.IsDebugEnabled) - { - log.Debug("unable to locate collection-filter query plan in cache; generating (" + collectionRole + " : " - + filterString + ")"); - } - plan = new FilterQueryPlan(filterString.ToQueryExpression(), collectionRole, shallow, enabledFilters, factory); + log.DebugFormat("unable to locate collection-filter query plan in cache; generating ({0} : {1})", collectionRole, queryExpression.Key); + plan = new FilterQueryPlan(queryExpression, collectionRole, shallow, enabledFilters, factory); planCache.Put(key, plan); } else { - if (log.IsDebugEnabled) - { - log.Debug("located collection-filter query plan in cache (" + collectionRole + " : " + filterString + ")"); - } + log.DebugFormat("located collection-filter query plan in cache ({0} : {1})", collectionRole, queryExpression.Key); + plan = CopyIfRequired(plan, queryExpression); } return plan; diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index 1cec6041a05..e6131efd52b 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -730,7 +730,17 @@ IASTNode CreateFromElement(string path, IASTNode pathNode, IASTNode alias, IASTN IASTNode CreateFromFilterElement(IASTNode filterEntity, IASTNode alias) { - FromElement fromElement = _currentFromClause.AddFromElement(filterEntity.Text, alias); + var fromElementFound = true; + + var fromElement = _currentFromClause.GetFromElement(alias.Text) ?? + _currentFromClause.GetFromElementByClassName(filterEntity.Text); + + if (fromElement == null) + { + fromElementFound = false; + fromElement = _currentFromClause.AddFromElement(filterEntity.Text, alias); + } + FromClause fromClause = fromElement.FromClause; IQueryableCollection persister = _sessionFactoryHelper.GetCollectionPersister(_collectionFilterRole); @@ -760,7 +770,10 @@ IASTNode CreateFromFilterElement(IASTNode filterEntity, IASTNode alias) { log.Debug("createFromFilterElement() : processed filter FROM element."); } - + + if (fromElementFound) + return (IASTNode) adaptor.Nil(); + return fromElement; } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs index f34c11b0c47..0707c72c694 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; + using Antlr.Runtime; using NHibernate.Hql.Ast.ANTLR.Util; @@ -376,5 +378,10 @@ public virtual void Resolve() } } } + + public FromElement GetFromElementByClassName(string className) + { + return _fromElementByClassAlias.Values.FirstOrDefault(variable => variable.ClassName == className); + } } } diff --git a/src/NHibernate/Impl/AbstractSessionImpl.cs b/src/NHibernate/Impl/AbstractSessionImpl.cs index ce4c8c2c56d..05c6184c4e0 100644 --- a/src/NHibernate/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Impl/AbstractSessionImpl.cs @@ -128,6 +128,17 @@ public virtual IList List(CriteriaImpl criteria) } public abstract IList ListFilter(object collection, string filter, QueryParameters parameters); + public IList ListFilter(object collection, IQueryExpression queryExpression, QueryParameters parameters) + { + var results = (IList)typeof(List<>).MakeGenericType(queryExpression.Type) + .GetConstructor(System.Type.EmptyTypes) + .Invoke(null); + + ListFilter(collection, queryExpression, parameters, results); + return results; + } + protected abstract void ListFilter(object collection, IQueryExpression queryExpression, QueryParameters parameters, IList results); + public abstract IList ListFilter(object collection, string filter, QueryParameters parameters); public abstract IEnumerable EnumerableFilter(object collection, string filter, QueryParameters parameters); public abstract IEnumerable EnumerableFilter(object collection, string filter, QueryParameters parameters); @@ -413,6 +424,8 @@ public void JoinTransaction() _factory.TransactionFactory.ExplicitJoinSystemTransaction(this); } + public abstract IQuery CreateFilter(object collection, IQueryExpression queryExpression); + internal IOuterJoinLoadable GetOuterJoinLoadable(string entityName) { using (new SessionIdLoggingContext(SessionId)) diff --git a/src/NHibernate/Impl/ExpressionQueryImpl.cs b/src/NHibernate/Impl/ExpressionQueryImpl.cs index 41ee788d6ae..f647ba2a076 100644 --- a/src/NHibernate/Impl/ExpressionQueryImpl.cs +++ b/src/NHibernate/Impl/ExpressionQueryImpl.cs @@ -8,6 +8,7 @@ using NHibernate.Hql.Ast.ANTLR; using NHibernate.Hql.Ast.ANTLR.Tree; using NHibernate.Hql.Ast.ANTLR.Util; +using NHibernate.Type; using NHibernate.Util; namespace NHibernate.Impl @@ -82,6 +83,56 @@ protected override IQueryExpression ExpandParameters(IDictionary typeList = Types; + int size = typeList.Count; + var result = new IType[size + 1]; + for (int i = 0; i < size; i++) + { + result[i + 1] = typeList[i]; + } + return result; + } + + public override object[] ValueArray() + { + IList valueList = Values; + int size = valueList.Count; + var result = new object[size + 1]; + for (int i = 0; i < size; i++) + { + result[i + 1] = valueList[i]; + } + return result; + } + } + internal class ExpandedQueryExpression : IQueryExpression { private readonly IASTNode _tree; @@ -225,4 +276,4 @@ private IList LocateParameters() return _nodes; } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Impl/SessionImpl.cs b/src/NHibernate/Impl/SessionImpl.cs index 486701f7cb1..8ac98f96ffe 100644 --- a/src/NHibernate/Impl/SessionImpl.cs +++ b/src/NHibernate/Impl/SessionImpl.cs @@ -536,14 +536,45 @@ public override void CloseSessionFromSystemTransaction() Dispose(true); } + public override IQuery CreateQuery(IQueryExpression queryExpression) + { + using (new SessionIdLoggingContext(SessionId)) + { + CheckAndUpdateSessionStatus(); + var plan = GetHQLQueryPlan(queryExpression, false); + var query = new ExpressionQueryImpl(plan.QueryExpression, this, plan.ParameterMetadata); + query.SetComment("[expression]"); + return query; + } + } + public override void List(IQueryExpression queryExpression, QueryParameters queryParameters, IList results) + { + List(queryExpression, queryParameters, results, null); + } + + protected override void ListFilter(object collection, IQueryExpression queryExpression, QueryParameters queryParameters, IList results) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + List(queryExpression, queryParameters, results, collection); + } + + private void List(IQueryExpression queryExpression, QueryParameters queryParameters, IList results, object filterConnection) { using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); queryParameters.ValidateParameters(); - var plan = GetHQLQueryPlan(queryExpression, false); - AutoFlushIfRequired(plan.QuerySpaces); + + var isFilter = filterConnection != null; + var plan = isFilter + ? GetFilterQueryPlan(filterConnection, queryExpression, queryParameters, false) + : GetHQLQueryPlan(queryExpression, false); + + // GetFilterQueryPlan has already auto flushed or fully flush. + if (!isFilter) + AutoFlushIfRequired(plan.QuerySpaces); bool success = false; using (SuspendAutoFlush()) //stops flush being called multiple times if this method is recursively called @@ -676,70 +707,88 @@ public void Lock(string entityName, object obj, LockMode lockMode) } } - /// - /// - /// - /// - /// - /// public IQuery CreateFilter(object collection, string queryString) { using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - CollectionFilterImpl filter = - new CollectionFilterImpl(queryString, collection, this, - GetFilterQueryPlan(collection, queryString, null, false).ParameterMetadata); + var plan = GetFilterQueryPlan(collection, queryString, null, false); + var filter = new CollectionFilterImpl(queryString, collection, this, plan.ParameterMetadata); //filter.SetComment(queryString); return filter; } } - private FilterQueryPlan GetFilterQueryPlan(object collection, string filter, QueryParameters parameters, bool shallow) + public override IQuery CreateFilter(object collection, IQueryExpression queryExpression) + { + using (new SessionIdLoggingContext(SessionId)) + { + CheckAndUpdateSessionStatus(); + + var plan = GetFilterQueryPlan(collection, queryExpression, null, false); + var filter = new ExpressionFilterImpl(plan.QueryExpression, collection, this, plan.ParameterMetadata); + return filter; + } + } + + private IQueryExpressionPlan GetFilterQueryPlan(object collection, IQueryExpression queryExpression, QueryParameters parameters, bool shallow) + { + return GetFilterQueryPlan(collection, parameters, shallow, null, queryExpression); + } + + private IQueryExpressionPlan GetFilterQueryPlan(object collection, string filter, QueryParameters parameters, bool shallow) + { + return GetFilterQueryPlan(collection, parameters, shallow, filter, null); + } + + private IQueryExpressionPlan GetFilterQueryPlan(object collection, QueryParameters parameters, bool shallow, + string filter, IQueryExpression queryExpression) { using (new SessionIdLoggingContext(SessionId)) { if (collection == null) - { - throw new ArgumentNullException("collection", "null collection passed to filter"); - } + throw new ArgumentNullException(nameof(collection), "null collection passed to filter"); + if (filter != null && queryExpression != null) + throw new ArgumentException($"Either {nameof(filter)} or {nameof(queryExpression)} must be specified, not both."); + if (filter == null && queryExpression == null) + throw new ArgumentException($"{nameof(filter)} and {nameof(queryExpression)} were both null."); - CollectionEntry entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleBeforeFlush = (entry == null) ? null : entry.LoadedPersister; + var entry = persistenceContext.GetCollectionEntryOrNull(collection); + var roleBeforeFlush = entry?.LoadedPersister; - FilterQueryPlan plan; + IQueryExpressionPlan plan; if (roleBeforeFlush == null) { // if it was previously unreferenced, we need to flush in order to // get its state into the database in order to execute query Flush(); entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleAfterFlush = (entry == null) ? null : entry.LoadedPersister; + var roleAfterFlush = entry?.LoadedPersister; if (roleAfterFlush == null) { throw new QueryException("The collection was unreferenced"); } - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleAfterFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleAfterFlush.Role, shallow, filter, queryExpression); } else { // otherwise, we only need to flush if there are in-memory changes // to the queried tables - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleBeforeFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleBeforeFlush.Role, shallow, filter, queryExpression); if (AutoFlushIfRequired(plan.QuerySpaces)) { // might need to run a different filter entirely after the flush // because the collection role may have changed entry = persistenceContext.GetCollectionEntryOrNull(collection); - ICollectionPersister roleAfterFlush = (entry == null) ? null : entry.LoadedPersister; + var roleAfterFlush = entry?.LoadedPersister; if (roleBeforeFlush != roleAfterFlush) { if (roleAfterFlush == null) { throw new QueryException("The collection was dereferenced"); } - plan = Factory.QueryPlanCache.GetFilterQueryPlan(filter, roleAfterFlush.Role, shallow, EnabledFilters); + plan = GetFilterQueryPlan(roleAfterFlush.Role, shallow, filter, queryExpression); } } } @@ -754,6 +803,13 @@ private FilterQueryPlan GetFilterQueryPlan(object collection, string filter, Que } } + private IQueryExpressionPlan GetFilterQueryPlan(string role, bool shallow, string filter, IQueryExpression queryExpression) + { + return filter == null + ? Factory.QueryPlanCache.GetFilterQueryPlan(queryExpression, role, shallow, EnabledFilters) + : Factory.QueryPlanCache.GetFilterQueryPlan(filter, role, shallow, EnabledFilters); + } + public override object Instantiate(string clazz, object id) { using (new SessionIdLoggingContext(SessionId)) @@ -1648,7 +1704,7 @@ private void Filter(object collection, string filter, QueryParameters queryParam using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = GetFilterQueryPlan(collection, filter, queryParameters, false); + var plan = GetFilterQueryPlan(collection, filter, queryParameters, false); bool success = false; using (SuspendAutoFlush()) //stops flush being called multiple times if this method is recursively called @@ -1700,7 +1756,7 @@ public override IEnumerable EnumerableFilter(object collection, string filter, Q using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = GetFilterQueryPlan(collection, filter, queryParameters, true); + var plan = GetFilterQueryPlan(collection, filter, queryParameters, true); return plan.PerformIterate(queryParameters, this); } } @@ -1710,7 +1766,7 @@ public override IEnumerable EnumerableFilter(object collection, string fil using (new SessionIdLoggingContext(SessionId)) { CheckAndUpdateSessionStatus(); - FilterQueryPlan plan = GetFilterQueryPlan(collection, filter, queryParameters, true); + var plan = GetFilterQueryPlan(collection, filter, queryParameters, true); return plan.PerformIterate(queryParameters, this); } } diff --git a/src/NHibernate/Impl/StatelessSessionImpl.cs b/src/NHibernate/Impl/StatelessSessionImpl.cs index 32c55b71928..bc0f9aced09 100644 --- a/src/NHibernate/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Impl/StatelessSessionImpl.cs @@ -110,6 +110,11 @@ public override void CloseSessionFromSystemTransaction() Dispose(true); } + public override IQuery CreateFilter(object collection, IQueryExpression queryExpression) + { + throw new NotSupportedException(); + } + public override void List(IQueryExpression queryExpression, QueryParameters queryParameters, IList results) { using (new SessionIdLoggingContext(SessionId)) @@ -197,6 +202,11 @@ public override IList ListFilter(object collection, string filter, QueryParamete throw new NotSupportedException(); } + protected override void ListFilter(object collection, IQueryExpression queryExpression, QueryParameters parameters, IList results) + { + throw new NotSupportedException(); + } + public override IList ListFilter(object collection, string filter, QueryParameters parameters) { throw new NotSupportedException(); diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 67410194604..57bd1f18513 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -36,6 +36,14 @@ public DefaultQueryProvider(ISessionImplementor session) _session = new WeakReference(session); } + public DefaultQueryProvider(ISessionImplementor session, object collection) + : this(session) + { + Collection = collection; + } + + public object Collection { get; } + protected virtual ISessionImplementor Session { get @@ -125,7 +133,14 @@ protected virtual NhLinqExpression PrepareQuery(Expression expression, out IQuer { var nhLinqExpression = new NhLinqExpression(expression, Session.Factory); - query = Session.CreateQuery(nhLinqExpression); + if (Collection == null) + { + query = Session.CreateQuery(nhLinqExpression); + } + else + { + query = Session.CreateFilter(Collection, nhLinqExpression); + } SetParameters(query, nhLinqExpression.ParameterValuesByName); SetResultTransformerAndAdditionalCriteria(query, nhLinqExpression, nhLinqExpression.ParameterValuesByName); diff --git a/src/NHibernate/Linq/NhQueryable.cs b/src/NHibernate/Linq/NhQueryable.cs index 1c6b93711a5..f1ef60f137a 100644 --- a/src/NHibernate/Linq/NhQueryable.cs +++ b/src/NHibernate/Linq/NhQueryable.cs @@ -27,7 +27,7 @@ public NhQueryable(ISessionImplementor session) // This constructor is called by our users, create a new IQueryExecutor. public NhQueryable(ISessionImplementor session, string entityName) - : base(QueryProviderFactory.CreateQueryProvider(session)) + : base(QueryProviderFactory.CreateQueryProvider(session, null)) { EntityName = entityName; } @@ -46,6 +46,12 @@ public NhQueryable(IQueryProvider provider, Expression expression, string entity EntityName = entityName; } + public NhQueryable(ISessionImplementor session, object collection) + : base(QueryProviderFactory.CreateQueryProvider(session, collection)) + { + EntityName = typeof(T).FullName; + } + public string EntityName { get; private set; } public override string ToString() diff --git a/src/NHibernate/Linq/QueryProviderFactory.cs b/src/NHibernate/Linq/QueryProviderFactory.cs index 52c4b8119c7..43cf502b055 100644 --- a/src/NHibernate/Linq/QueryProviderFactory.cs +++ b/src/NHibernate/Linq/QueryProviderFactory.cs @@ -9,16 +9,20 @@ static class QueryProviderFactory /// Builds a new query provider. /// /// A session. + /// If the query is to be filtered as belonging to an entity collection, the collection. /// The new query provider instance. - public static INhQueryProvider CreateQueryProvider(ISessionImplementor session) + public static INhQueryProvider CreateQueryProvider(ISessionImplementor session, object collection) { if (session.Factory.Settings.LinqQueryProviderType == null) { - return new DefaultQueryProvider(session); + return new DefaultQueryProvider(session, collection); } else { - return Activator.CreateInstance(session.Factory.Settings.LinqQueryProviderType, session) as INhQueryProvider; + // For backward compatibility, prioritize using the version without collection. + return (collection == null + ? Activator.CreateInstance(session.Factory.Settings.LinqQueryProviderType, session) + : Activator.CreateInstance(session.Factory.Settings.LinqQueryProviderType, session, collection)) as INhQueryProvider; } } } diff --git a/src/NHibernate/Mapping/ByCode/ModelMapper.cs b/src/NHibernate/Mapping/ByCode/ModelMapper.cs index 17f460188ed..6551216b20f 100644 --- a/src/NHibernate/Mapping/ByCode/ModelMapper.cs +++ b/src/NHibernate/Mapping/ByCode/ModelMapper.cs @@ -1240,9 +1240,9 @@ private void MapIdBag(MemberInfo member, PropertyPath propertyPath, System.Type { System.Type collectionElementType = GetCollectionElementTypeOrThrow(propertiesContainerType, member, propertyType); ICollectionElementRelationMapper cert = DetermineCollectionElementRelationType(member, propertyPath, collectionElementType); - if(cert is OneToManyRelationMapper) + if (cert is OneToManyRelationMapper) { - throw new NotSupportedException("id-bag does not suppot one-to-many relation"); + throw new NotSupportedException("id-bag does not support one-to-many relation"); } propertiesContainer.IdBag(member, collectionPropertiesMapper => {