diff --git a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs index a3e191f6d2d..878a45e1a41 100644 --- a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs @@ -15,6 +15,7 @@ using NHibernate.Cache; using NHibernate.Cfg; using NHibernate.Linq; +using NHibernate.Loader; using NHibernate.Multi; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; @@ -24,9 +25,17 @@ namespace NHibernate.Test.CacheTest { using System.Threading.Tasks; using System.Threading; - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class BatchableCacheFixtureAsync : TestCase { + private readonly BatchFetchStyle _fetchStyle; + + public BatchableCacheFixtureAsync(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + protected override string[] Mappings => new[] { "CacheTest.ReadOnly.hbm.xml", @@ -43,6 +52,7 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.UseQueryCache, "true"); configuration.SetProperty(Environment.GenerateStatistics, "true"); configuration.SetProperty(Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Environment.BatchFetchStyle, _fetchStyle.ToString()); } protected override void OnSetUp() diff --git a/src/NHibernate.Test/Async/CacheTest/BatchableCacheSubclassFixture.cs b/src/NHibernate.Test/Async/CacheTest/BatchableCacheSubclassFixture.cs index 7ac6d663aee..1bbfbda902b 100644 --- a/src/NHibernate.Test/Async/CacheTest/BatchableCacheSubclassFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/BatchableCacheSubclassFixture.cs @@ -15,15 +15,24 @@ using NHibernate.Cache; using NHibernate.Cfg; using NHibernate.DomainModel; +using NHibernate.Loader; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; namespace NHibernate.Test.CacheTest { using System.Threading.Tasks; - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class BatchableCacheSubclassFixtureAsync : TestCase { + private readonly BatchFetchStyle _fetchStyle; + + public BatchableCacheSubclassFixtureAsync(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + protected override string[] Mappings { get @@ -56,6 +65,7 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Cfg.Environment.UseSecondLevelCache, "true"); configuration.SetProperty(Cfg.Environment.UseQueryCache, "true"); configuration.SetProperty(Cfg.Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Cfg.Environment.BatchFetchStyle, _fetchStyle.ToString()); } protected override void OnSetUp() diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3142/ChildrenTest.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3142/ChildrenTest.cs index 7d0e2aa0c2f..386428efd12 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH3142/ChildrenTest.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3142/ChildrenTest.cs @@ -11,15 +11,31 @@ using System; using System.Collections; using System.Collections.Generic; +using NHibernate.Cfg; using NHibernate.Driver; +using NHibernate.Loader; using NUnit.Framework; namespace NHibernate.Test.NHSpecificTest.NH3142 { using System.Threading.Tasks; - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class ChildrenTestAsync : BugTestCase { + private readonly BatchFetchStyle _fetchStyle; + + public ChildrenTestAsync(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(Cfg.Environment.BatchFetchStyle, _fetchStyle.ToString()); + } + protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory) { return !(factory.ConnectionProvider.Driver is OracleManagedDataClientDriver); diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs new file mode 100644 index 00000000000..be33442597b --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs @@ -0,0 +1,201 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Loader; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.NHSpecificTest.NH3530 +{ + using System.Threading.Tasks; + using System.Threading; + //NH-3530 (GH-1316) + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] + public class BatchFetchStyleFixtureAsync : TestCaseMappingByCode + { + private readonly BatchFetchStyle _fetchStyle; + private readonly List _ids = new List(); + + public BatchFetchStyleFixtureAsync(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(Environment.BatchFetchStyle, _fetchStyle.ToString()); + } + + [Test] + public async Task CanLoadEntityAsync() + { + await (PrepareEntitiesAsync(2)); + + using (var session = OpenSession()) + { + var proxy = await (session.LoadAsync(_ids[0])); + var result = await (session.GetAsync(_ids[1])); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + //Assert.That(NHibernateUtil.IsInitialized(proxy), Is.True); + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [KnownBug("GH-2960")] + [Test] + public async Task CanLoadComponentEntityAsync() + { + await (PrepareComponentEntitiesAsync(2)); + + using (var session = OpenSession()) + { + var proxy = await (session.LoadAsync(_ids[0])); + var result = await (session.GetAsync(_ids[1])); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [Test] + public async Task CanLoadComponentIdEntityAsync() + { + await (PrepareComponentIdEntitiesAsync(2)); + + using (var session = OpenSession()) + { + var proxy = await (session.LoadAsync(_ids[0])); + var result = await (session.GetAsync(_ids[1])); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + public async Task CanLoadBatchAsync(int loadCount) + { + await (PrepareEntitiesAsync(5)); + + using (var session = OpenSession()) + { + foreach (var id in _ids.Take(loadCount)) + { + await (session.LoadAsync(id)); + } + + var result = await (session.GetAsync(_ids[0])); + var result2 = await (session.GetAsync(_ids[1])); + var last = await (session.GetAsync(_ids.Last())); + + var count = result.Children.Count; + Assert.That(result.Name, Is.Not.Null); + Assert.That(last.Name, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result2.Children), Is.True); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + transaction.Commit(); + } + } + + protected override HbmMapping GetMappings() + { + return EntityMappings.CreateMapping(); + } + + private async Task PrepareEntitiesAsync(int count, CancellationToken cancellationToken = default(CancellationToken)) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new Entity { Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + _ids.Add((Guid) await (session.SaveAsync(entity, cancellationToken))); + } + + await (transaction.CommitAsync(cancellationToken)); + } + } + + private async Task PrepareComponentEntitiesAsync(int count, CancellationToken cancellationToken = default(CancellationToken)) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new EntityComponent { Id1 = i, Id2 = i + 1, Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + + _ids.Add(await (session.SaveAsync(entity, cancellationToken))); + } + + await (transaction.CommitAsync(cancellationToken)); + } + } + + private async Task PrepareComponentIdEntitiesAsync(int count, CancellationToken cancellationToken = default(CancellationToken)) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new EntityComponentId { Id = new ComponentId { Id1 = i, Id2 = i + 1 }, Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + _ids.Add(await (session.SaveAsync(entity, cancellationToken))); + } + + await (transaction.CommitAsync(cancellationToken)); + } + } + + private static void AddChildren(IList list, int count) where T : new() + { + for (int i = 0; i < count; i++) + { + list.Add(new T()); + } + } + } +} diff --git a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs index e8df7dca2de..bf7031c936f 100644 --- a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs @@ -5,6 +5,7 @@ using NHibernate.Cache; using NHibernate.Cfg; using NHibernate.Linq; +using NHibernate.Loader; using NHibernate.Multi; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; @@ -12,9 +13,17 @@ namespace NHibernate.Test.CacheTest { - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class BatchableCacheFixture : TestCase { + private readonly BatchFetchStyle _fetchStyle; + + public BatchableCacheFixture(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + protected override string[] Mappings => new[] { "CacheTest.ReadOnly.hbm.xml", @@ -31,6 +40,7 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.UseQueryCache, "true"); configuration.SetProperty(Environment.GenerateStatistics, "true"); configuration.SetProperty(Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Environment.BatchFetchStyle, _fetchStyle.ToString()); } protected override void OnSetUp() diff --git a/src/NHibernate.Test/CacheTest/BatchableCacheSubclassFixture.cs b/src/NHibernate.Test/CacheTest/BatchableCacheSubclassFixture.cs index f816113d5c2..c187af254d3 100644 --- a/src/NHibernate.Test/CacheTest/BatchableCacheSubclassFixture.cs +++ b/src/NHibernate.Test/CacheTest/BatchableCacheSubclassFixture.cs @@ -5,14 +5,23 @@ using NHibernate.Cache; using NHibernate.Cfg; using NHibernate.DomainModel; +using NHibernate.Loader; using NHibernate.Test.CacheTest.Caches; using NUnit.Framework; namespace NHibernate.Test.CacheTest { - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class BatchableCacheSubclassFixture : TestCase { + private readonly BatchFetchStyle _fetchStyle; + + public BatchableCacheSubclassFixture(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + protected override string[] Mappings { get @@ -45,6 +54,7 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Cfg.Environment.UseSecondLevelCache, "true"); configuration.SetProperty(Cfg.Environment.UseQueryCache, "true"); configuration.SetProperty(Cfg.Environment.CacheProvider, typeof(BatchableCacheProvider).AssemblyQualifiedName); + configuration.SetProperty(Cfg.Environment.BatchFetchStyle, _fetchStyle.ToString()); } protected override void OnSetUp() diff --git a/src/NHibernate.Test/NHSpecificTest/NH3142/ChildrenTest.cs b/src/NHibernate.Test/NHSpecificTest/NH3142/ChildrenTest.cs index b6b77b7f8a5..c9d982b2a4f 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3142/ChildrenTest.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH3142/ChildrenTest.cs @@ -1,14 +1,30 @@ using System; using System.Collections; using System.Collections.Generic; +using NHibernate.Cfg; using NHibernate.Driver; +using NHibernate.Loader; using NUnit.Framework; namespace NHibernate.Test.NHSpecificTest.NH3142 { - [TestFixture] + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] public class ChildrenTest : BugTestCase { + private readonly BatchFetchStyle _fetchStyle; + + public ChildrenTest(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(Cfg.Environment.BatchFetchStyle, _fetchStyle.ToString()); + } + protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory) { return !(factory.ConnectionProvider.Driver is OracleManagedDataClientDriver); diff --git a/src/NHibernate.Test/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs new file mode 100644 index 00000000000..45ca778c972 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3530/BatchFetchStyleFixture.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Loader; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.NHSpecificTest.NH3530 +{ + //NH-3530 (GH-1316) + [TestFixture(BatchFetchStyle.Dynamic)] + [TestFixture(BatchFetchStyle.Legacy)] + public class BatchFetchStyleFixture : TestCaseMappingByCode + { + private readonly BatchFetchStyle _fetchStyle; + private readonly List _ids = new List(); + + public BatchFetchStyleFixture(BatchFetchStyle fetchStyle) + { + _fetchStyle = fetchStyle; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(Environment.BatchFetchStyle, _fetchStyle.ToString()); + } + + [Test] + public void CanLoadEntity() + { + PrepareEntities(2); + + using (var session = OpenSession()) + { + var proxy = session.Load(_ids[0]); + var result = session.Get(_ids[1]); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + //Assert.That(NHibernateUtil.IsInitialized(proxy), Is.True); + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [KnownBug("GH-2960")] + [Test] + public void CanLoadComponentEntity() + { + PrepareComponentEntities(2); + + using (var session = OpenSession()) + { + var proxy = session.Load(_ids[0]); + var result = session.Get(_ids[1]); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [Test] + public void CanLoadComponentIdEntity() + { + PrepareComponentIdEntities(2); + + using (var session = OpenSession()) + { + var proxy = session.Load(_ids[0]); + var result = session.Get(_ids[1]); + + Assert.That(result.Name, Is.Not.Null); + + var childrenCount = result.Children.Count; + Assert.That(NHibernateUtil.IsInitialized(proxy.Children), Is.True); + Assert.That(childrenCount, Is.EqualTo(4)); + } + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + public void CanLoadBatch(int loadCount) + { + PrepareEntities(5); + + using (var session = OpenSession()) + { + foreach (var id in _ids.Take(loadCount)) + { + session.Load(id); + } + + var result = session.Get(_ids[0]); + var result2 = session.Get(_ids[1]); + var last = session.Get(_ids.Last()); + + var count = result.Children.Count; + Assert.That(result.Name, Is.Not.Null); + Assert.That(last.Name, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result2.Children), Is.True); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + transaction.Commit(); + } + } + + protected override HbmMapping GetMappings() + { + return EntityMappings.CreateMapping(); + } + + private void PrepareEntities(int count) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new Entity { Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + _ids.Add((Guid) session.Save(entity)); + } + + transaction.Commit(); + } + } + + private void PrepareComponentEntities(int count) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new EntityComponent { Id1 = i, Id2 = i + 1, Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + + _ids.Add(session.Save(entity)); + } + + transaction.Commit(); + } + } + + private void PrepareComponentIdEntities(int count) + { + _ids.Clear(); + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < count; i++) + { + var entity = new EntityComponentId { Id = new ComponentId { Id1 = i, Id2 = i + 1 }, Name = "some name" + 1 }; + AddChildren(entity.Children, 4); + _ids.Add(session.Save(entity)); + } + + transaction.Commit(); + } + } + + private static void AddChildren(IList list, int count) where T : new() + { + for (int i = 0; i < count; i++) + { + list.Add(new T()); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3530/Entities.cs b/src/NHibernate.Test/NHSpecificTest/NH3530/Entities.cs new file mode 100644 index 00000000000..8a864228b44 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3530/Entities.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace NHibernate.Test.NHSpecificTest.NH3530 +{ + public class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual IList Children { get; set; } = new List(); + } + + public class EntityComponent + { + public virtual int Id1 { get; set; } + public virtual int Id2 { get; set; } + public virtual string Name { get; set; } + public virtual IList Children { get; set; } = new List(); + + public override bool Equals(object obj) + { + return obj == this; + } + + public override int GetHashCode() + { + return RuntimeHelpers.GetHashCode(this); + } + } + + public class ComponentId + { + public int Id1 { get; set; } + public int Id2 { get; set; } + + protected bool Equals(ComponentId other) + { + return Id1 == other.Id1 && Id2 == other.Id2; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ComponentId) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (Id1 * 397) ^ Id2; + } + } + } + + public class EntityComponentId + { + public virtual ComponentId Id { get; set; } + + public virtual string Name { get; set; } + public virtual IList Children { get; set; } = new List(); + + public override bool Equals(object obj) + { + return obj == this; + } + + public override int GetHashCode() + { + return RuntimeHelpers.GetHashCode(this); + } + } + + public class Child + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual Guid ParentId { get; set; } + } + + public class ChildForComponent + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual int ParentId1 { get; set; } + public virtual int ParentId2 { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3530/EntityMappings.cs b/src/NHibernate.Test/NHSpecificTest/NH3530/EntityMappings.cs new file mode 100644 index 00000000000..3ae312a2d84 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3530/EntityMappings.cs @@ -0,0 +1,108 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; + +namespace NHibernate.Test.NHSpecificTest.NH3530 +{ + class EntityMappings + { + public static HbmMapping CreateMapping() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.BatchSize(3); + rc.Bag( + x => x.Children, + m => + { + m.Key( + km => + { + km.Column("ParentId"); + km.ForeignKey("none"); + }); + m.BatchSize(10); + m.Cascade(Mapping.ByCode.Cascade.All); + }, + xr => xr.OneToMany()); + }); + + mapper.Class( + rc => + { + rc.ComposedId( + x => + { + x.Property(t => t.Id1); + x.Property(e => e.Id2); + }); + rc.Property(x => x.Name); + rc.BatchSize(3); + rc.Bag( + x => x.Children, + m => + { + m.Key( + km => + { + km.Columns(x => x.Name("ParentId1"), x => x.Name("ParentId2")); + km.ForeignKey("none"); + }); + m.BatchSize(10); + m.Cascade(Mapping.ByCode.Cascade.All); + }, + xr => xr.OneToMany()); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + x => x.Id, + m => + { + m.Property(t => t.Id1); + m.Property(e => e.Id2); + } + ); + rc.Property(x => x.Name); + rc.BatchSize(3); + rc.Bag( + x => x.Children, + m => + { + m.Key( + km => + { + km.Columns(x => x.Name("ParentId1"), x => x.Name("ParentId2")); + km.ForeignKey("none"); + }); + m.BatchSize(10); + m.Cascade(Mapping.ByCode.Cascade.All); + }, + xr => xr.OneToMany()); + }); + + mapper.Class( + rc => + { + rc.Id(m => m.Id, m => m.Generator(Generators.Guid)); + rc.Property(m => m.Name); + rc.Property(m => m.ParentId); + }); + + mapper.Class( + rc => + { + rc.Id(m => m.Id, m => m.Generator(Generators.Guid)); + rc.Property(m => m.Name); + rc.Property(m => m.ParentId1); + rc.Property(m => m.ParentId2); + }); + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate/Async/Loader/Collection/AbstractBatchingCollectionInitializer.cs b/src/NHibernate/Async/Loader/Collection/AbstractBatchingCollectionInitializer.cs new file mode 100644 index 00000000000..6d47e0f3833 --- /dev/null +++ b/src/NHibernate/Async/Loader/Collection/AbstractBatchingCollectionInitializer.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + using System.Threading.Tasks; + using System.Threading; + public abstract partial class AbstractBatchingCollectionInitializer : ICollectionInitializer + { + + public abstract Task InitializeAsync(object id, ISessionImplementor session, CancellationToken cancellationToken); + } +} diff --git a/src/NHibernate/Async/Loader/Collection/BatchingCollectionInitializer.cs b/src/NHibernate/Async/Loader/Collection/BatchingCollectionInitializer.cs index 5300a05df53..574c893ee01 100644 --- a/src/NHibernate/Async/Loader/Collection/BatchingCollectionInitializer.cs +++ b/src/NHibernate/Async/Loader/Collection/BatchingCollectionInitializer.cs @@ -18,14 +18,14 @@ namespace NHibernate.Loader.Collection { using System.Threading.Tasks; using System.Threading; - public partial class BatchingCollectionInitializer : ICollectionInitializer + public partial class BatchingCollectionInitializer : AbstractBatchingCollectionInitializer { - public async Task InitializeAsync(object id, ISessionImplementor session, CancellationToken cancellationToken) + public override async Task InitializeAsync(object id, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); object[] batch = - await (session.PersistenceContext.BatchFetchQueue.GetCollectionBatchAsync(collectionPersister, id, batchSizes[0], cancellationToken)).ConfigureAwait(false); + await (session.PersistenceContext.BatchFetchQueue.GetCollectionBatchAsync(CollectionPersister, id, batchSizes[0], cancellationToken)).ConfigureAwait(false); for (int i = 0; i < batchSizes.Length; i++) { @@ -34,12 +34,12 @@ public async Task InitializeAsync(object id, ISessionImplementor session, Cancel { object[] smallBatch = new object[smallBatchSize]; Array.Copy(batch, 0, smallBatch, 0, smallBatchSize); - await (loaders[i].LoadCollectionBatchAsync(session, smallBatch, collectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); + await (loaders[i].LoadCollectionBatchAsync(session, smallBatch, CollectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); return; //EARLY EXIT! } } - await (loaders[batchSizes.Length - 1].LoadCollectionAsync(session, id, collectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); + await (loaders[batchSizes.Length - 1].LoadCollectionAsync(session, id, CollectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Loader/Collection/DynamicBatchingCollectionInitializer.cs b/src/NHibernate/Async/Loader/Collection/DynamicBatchingCollectionInitializer.cs new file mode 100644 index 00000000000..95358a5055d --- /dev/null +++ b/src/NHibernate/Async/Loader/Collection/DynamicBatchingCollectionInitializer.cs @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + using System.Threading.Tasks; + using System.Threading; + internal partial class DynamicBatchingCollectionInitializer : AbstractBatchingCollectionInitializer + { + + public override async Task InitializeAsync(object id, ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + // first, figure out how many batchable ids we have... + object[] batch = await (session.PersistenceContext.BatchFetchQueue.GetCollectionBatchAsync(CollectionPersister, id, _maxBatchSize, cancellationToken)).ConfigureAwait(false); + var numberOfIds = DynamicBatchingHelper.GetIdsToLoad(batch, out var idsToLoad); + if (numberOfIds <= 1) + { + await (_singleKeyLoader.LoadCollectionAsync(session, id, CollectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); + return; + } + + await (_batchLoader.LoadCollectionBatchAsync(session, idsToLoad, CollectionPersister.KeyType, cancellationToken)).ConfigureAwait(false); + } + } +} diff --git a/src/NHibernate/Async/Loader/Entity/AbstractBatchingEntityLoader.cs b/src/NHibernate/Async/Loader/Entity/AbstractBatchingEntityLoader.cs new file mode 100644 index 00000000000..ab6e66c90c1 --- /dev/null +++ b/src/NHibernate/Async/Loader/Entity/AbstractBatchingEntityLoader.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections; +using NHibernate.Engine; +using NHibernate.Persister.Entity; +using NHibernate.Type; +using NHibernate.Util; + +namespace NHibernate.Loader.Entity +{ + using System.Threading.Tasks; + using System.Threading; + public abstract partial class AbstractBatchingEntityLoader : IUniqueEntityLoader + { + + public abstract Task LoadAsync(object id, object optionalObject, ISessionImplementor session, CancellationToken cancellationToken); + } +} diff --git a/src/NHibernate/Async/Loader/Entity/AbstractEntityLoader.cs b/src/NHibernate/Async/Loader/Entity/AbstractEntityLoader.cs index 88c031784c9..b34ae933180 100644 --- a/src/NHibernate/Async/Loader/Entity/AbstractEntityLoader.cs +++ b/src/NHibernate/Async/Loader/Entity/AbstractEntityLoader.cs @@ -80,4 +80,4 @@ protected virtual async Task LoadAsync(ISessionImplementor session, obje } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Loader/Entity/BatchingEntityLoader.cs b/src/NHibernate/Async/Loader/Entity/BatchingEntityLoader.cs index 57e6103ea01..42cd9f085fd 100644 --- a/src/NHibernate/Async/Loader/Entity/BatchingEntityLoader.cs +++ b/src/NHibernate/Async/Loader/Entity/BatchingEntityLoader.cs @@ -20,14 +20,14 @@ namespace NHibernate.Loader.Entity { using System.Threading.Tasks; using System.Threading; - public partial class BatchingEntityLoader : IUniqueEntityLoader + public partial class BatchingEntityLoader : AbstractBatchingEntityLoader { - public async Task LoadAsync(object id, object optionalObject, ISessionImplementor session, CancellationToken cancellationToken) + public override async Task LoadAsync(object id, object optionalObject, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); object[] batch = - await (session.PersistenceContext.BatchFetchQueue.GetEntityBatchAsync(persister, id, batchSizes[0], cancellationToken)).ConfigureAwait(false); + await (session.PersistenceContext.BatchFetchQueue.GetEntityBatchAsync(Persister, id, batchSizes[0], cancellationToken)).ConfigureAwait(false); for (int i = 0; i < batchSizes.Length - 1; i++) { @@ -38,7 +38,7 @@ public async Task LoadAsync(object id, object optionalObject, ISessionIm Array.Copy(batch, 0, smallBatch, 0, smallBatchSize); IList results = - await (loaders[i].LoadEntityBatchAsync(session, smallBatch, idType, optionalObject, persister.EntityName, id, persister, cancellationToken)).ConfigureAwait(false); + await (loaders[i].LoadEntityBatchAsync(session, smallBatch, idType, optionalObject, Persister.EntityName, id, Persister, cancellationToken)).ConfigureAwait(false); return GetObjectFromList(results, id, session); //EARLY EXIT } @@ -47,4 +47,4 @@ public async Task LoadAsync(object id, object optionalObject, ISessionIm return await (((IUniqueEntityLoader) loaders[batchSizes.Length - 1]).LoadAsync(id, optionalObject, session, cancellationToken)).ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Loader/Entity/DynamicBatchingEntityLoader.cs b/src/NHibernate/Async/Loader/Entity/DynamicBatchingEntityLoader.cs new file mode 100644 index 00000000000..fc1c7bdd0a0 --- /dev/null +++ b/src/NHibernate/Async/Loader/Entity/DynamicBatchingEntityLoader.cs @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Entity; + +namespace NHibernate.Loader.Entity +{ + using System.Threading.Tasks; + using System.Threading; + internal partial class DynamicBatchingEntityLoader : AbstractBatchingEntityLoader + { + + public override async Task LoadAsync(object id, object optionalObject, ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + object[] batch = await (session.PersistenceContext.BatchFetchQueue.GetEntityBatchAsync(Persister, id, _maxBatchSize, cancellationToken)).ConfigureAwait(false); + + var numberOfIds = DynamicBatchingHelper.GetIdsToLoad(batch, out var idsToLoad); + if (numberOfIds <= 1) + { + return await (_singleKeyLoader.LoadAsync(id, optionalObject, session, cancellationToken)).ConfigureAwait(false); + } + + QueryParameters qp = BuildQueryParameters(id, idsToLoad, optionalObject); + IList results = await (_dynamicEntityLoader.DoEntityBatchFetchAsync(session, qp, cancellationToken)).ConfigureAwait(false); + return GetObjectFromList(results, id, session); + } + } +} diff --git a/src/NHibernate/Async/Loader/Entity/DynamicEntityLoader.cs b/src/NHibernate/Async/Loader/Entity/DynamicEntityLoader.cs new file mode 100644 index 00000000000..0d1e49caf25 --- /dev/null +++ b/src/NHibernate/Async/Loader/Entity/DynamicEntityLoader.cs @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Param; +using NHibernate.Persister.Entity; +using NHibernate.SqlCommand; +using NHibernate.Util; + +namespace NHibernate.Loader.Entity +{ + using System.Threading.Tasks; + using System.Threading; + internal partial class DynamicEntityLoader : EntityLoader + { + + public virtual Task DoEntityBatchFetchAsync(ISessionImplementor session, QueryParameters queryParameters, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return LoadEntityBatchAsync(session, persister, queryParameters, cancellationToken); + } + } +} diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 7629989c0ae..128130c5b74 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -1149,25 +1149,46 @@ protected Task GetResultSetAsync(DbCommand st, bool autoDiscoverTy /// /// Called by subclasses that batch load entities /// - protected internal async Task LoadEntityBatchAsync(ISessionImplementor session, object[] ids, IType idType, + protected internal Task LoadEntityBatchAsync(ISessionImplementor session, object[] ids, IType idType, object optionalObject, string optionalEntityName, object optionalId, IEntityPersister persister, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + IType[] types = new IType[ids.Length]; + ArrayHelper.Fill(types, idType); + var queryParameters = new QueryParameters( + types, + ids, + optionalObject, + optionalEntityName, + optionalId); + return LoadEntityBatchAsync(session, persister, queryParameters, cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + protected internal async Task LoadEntityBatchAsync(ISessionImplementor session, IEntityPersister persister, QueryParameters queryParameters, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + var ids = queryParameters.PositionalParameterValues; if (Log.IsDebugEnabled()) { Log.Debug("batch loading entity: {0}", MessageHelper.InfoString(persister, ids, Factory)); } - IType[] types = new IType[ids.Length]; - ArrayHelper.Fill(types, idType); - IList result; try { - result = - await (DoQueryAndInitializeNonLazyCollectionsAsync(session, - new QueryParameters(types, ids, optionalObject, optionalEntityName, - optionalId), false, cancellationToken)).ConfigureAwait(false); + var results = await (DoQueryAndInitializeNonLazyCollectionsAsync(session, queryParameters, false, cancellationToken)).ConfigureAwait(false); + Log.Debug("done entity batch load"); + return results; } catch (OperationCanceledException) { throw; } catch (HibernateException) @@ -1181,9 +1202,6 @@ protected Task GetResultSetAsync(DbCommand st, bool autoDiscoverTy + MessageHelper.InfoString(persister, ids, Factory), SqlString); // NH: Hibernate3 passes EntityPersisters[0] instead of persister, I think it's wrong. } - - Log.Debug("done entity batch load"); - return result; } /// diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs index 65783346714..d885f5393a4 100644 --- a/src/NHibernate/Cfg/Environment.cs +++ b/src/NHibernate/Cfg/Environment.cs @@ -234,6 +234,11 @@ public static string Version public const string DefaultBatchFetchSize = "default_batch_fetch_size"; + /// + /// to use. + /// + public const string BatchFetchStyle = "batch_fetch_style"; + public const string CollectionTypeFactoryClass = "collectiontype.factory_class"; public const string LinqToHqlGeneratorsRegistry = "linqtohql.generatorsregistry"; diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs index 474f51cc7aa..38c7ed31fdf 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -10,6 +10,9 @@ using NHibernate.Hql; using NHibernate.Linq.Functions; using NHibernate.Linq.Visitors; +using NHibernate.Loader; +using NHibernate.Loader.Collection; +using NHibernate.Loader.Entity; using NHibernate.MultiTenancy; using NHibernate.Transaction; @@ -221,5 +224,8 @@ internal string GetFullCacheRegionName(string name) public IMultiTenancyConnectionProvider MultiTenancyConnectionProvider { get; internal set; } public int QueryPlanCacheParameterMetadataMaxSize { get; internal set; } public int QueryPlanCacheMaxSize { get; internal set; } + public BatchFetchStyle BatchFetchStyle { get; internal set; } + public BatchingEntityLoaderBuilder BatchingEntityLoaderBuilder { get; internal set; } + public BatchingCollectionInitializerBuilder BatchingCollectionInitializationBuilder { get; internal set; } } } diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs index f0a10519590..02da6aa2ab5 100644 --- a/src/NHibernate/Cfg/SettingsFactory.cs +++ b/src/NHibernate/Cfg/SettingsFactory.cs @@ -13,6 +13,9 @@ using NHibernate.Linq; using NHibernate.Linq.Functions; using NHibernate.Linq.Visitors; +using NHibernate.Loader; +using NHibernate.Loader.Collection; +using NHibernate.Loader.Entity; using NHibernate.MultiTenancy; using NHibernate.Transaction; using NHibernate.Util; @@ -344,6 +347,10 @@ public Settings BuildSettings(IDictionary properties) settings.MultiTenancyConnectionProvider = CreateMultiTenancyConnectionProvider(properties); } + settings.BatchFetchStyle = PropertiesHelper.GetEnum(Environment.BatchFetchStyle, properties, BatchFetchStyle.Legacy); + settings.BatchingEntityLoaderBuilder = GetBatchingEntityLoaderBuilder(settings.BatchFetchStyle); + settings.BatchingCollectionInitializationBuilder = GetBatchingCollectionInitializationBuilder(settings.BatchFetchStyle); + return settings; } @@ -369,6 +376,36 @@ private ICacheReadWriteLockFactory GetReadWriteLockFactory(string lockFactory) } } + private BatchingCollectionInitializerBuilder GetBatchingCollectionInitializationBuilder(BatchFetchStyle batchFetchStyle) + { + switch (batchFetchStyle) + { + case BatchFetchStyle.Legacy: + return new LegacyBatchingCollectionInitializerBuilder(); + // case BatchFetchStyle.PADDED: + // break; + case BatchFetchStyle.Dynamic: + return new DynamicBatchingCollectionInitializerBuilder(); + default: + throw new ArgumentOutOfRangeException(nameof(batchFetchStyle), batchFetchStyle, null); + } + } + + private BatchingEntityLoaderBuilder GetBatchingEntityLoaderBuilder(BatchFetchStyle batchFetchStyle) + { + switch (batchFetchStyle) + { + case BatchFetchStyle.Legacy: + return new LegacyBatchingEntityLoaderBuilder(); + // case BatchFetchStyle.PADDED: + // break; + case BatchFetchStyle.Dynamic: + return new DynamicBatchingEntityLoaderBuilder(); + default: + throw new ArgumentOutOfRangeException(nameof(batchFetchStyle), batchFetchStyle, null); + } + } + private static IBatcherFactory CreateBatcherFactory(IDictionary properties, int batchSize, IConnectionProvider connectionProvider) { System.Type tBatcher = typeof (NonBatchingBatcherFactory); diff --git a/src/NHibernate/Criterion/InExpression.cs b/src/NHibernate/Criterion/InExpression.cs index 54409ba9f31..ec497739169 100644 --- a/src/NHibernate/Criterion/InExpression.cs +++ b/src/NHibernate/Criterion/InExpression.cs @@ -67,28 +67,32 @@ public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteri list.AddRange(criteriaQuery.NewQueryParameter(typedValue)); } - var bogusParam = Parameter.Placeholder; + return GetSqlString(columns, Values.Length, list, criteriaQuery.Factory.Dialect); + } - var sqlString = GetSqlString(criteriaQuery, columns, bogusParam); - sqlString.SubstituteBogusParameters(list, bogusParam); + internal static SqlString GetSqlString(object[] columns, int paramsCount, IReadOnlyList parameters, Dialect.Dialect dialect) + { + var bogusParam = Parameter.Placeholder; + var sqlString = GetSqlString(columns, paramsCount, bogusParam, dialect); + sqlString.SubstituteBogusParameters(parameters, bogusParam); return sqlString; } - private SqlString GetSqlString(ICriteriaQuery criteriaQuery, SqlString[] columns, Parameter bogusParam) + private static SqlString GetSqlString(object[] columns, int paramsCount, Parameter bogusParam, Dialect.Dialect dialect) { - if (columns.Length <= 1 || criteriaQuery.Factory.Dialect.SupportsRowValueConstructorSyntaxInInList) + if (columns.Length <= 1 || dialect.SupportsRowValueConstructorSyntaxInInList) { var wrapInParens = columns.Length > 1; const string comaSeparator = ", "; var singleValueParam = SqlStringHelper.Repeat(new SqlString(bogusParam), columns.Length, comaSeparator, wrapInParens); - var parameters = SqlStringHelper.Repeat(singleValueParam, Values.Length, comaSeparator, wrapInParens: false); + var parameters = SqlStringHelper.Repeat(singleValueParam, paramsCount, comaSeparator, wrapInParens: false); //single column: col1 in (?, ?) //multi column: (col1, col2) in ((?, ?), (?, ?)) return new SqlString( wrapInParens ? StringHelper.OpenParen : string.Empty, - SqlStringHelper.Join(comaSeparator, columns), + SqlStringHelper.JoinParts(comaSeparator, columns), wrapInParens ? StringHelper.ClosedParen : string.Empty, " in (", parameters, @@ -102,7 +106,7 @@ private SqlString GetSqlString(ICriteriaQuery criteriaQuery, SqlString[] columns "= ", bogusParam, " ) "); - cols = SqlStringHelper.Repeat(cols, Values.Length, " or ", wrapInParens: Values.Length > 1); + cols = SqlStringHelper.Repeat(cols, paramsCount, " or ", wrapInParens: paramsCount > 1); return cols; } diff --git a/src/NHibernate/Loader/BatchFetchStyle.cs b/src/NHibernate/Loader/BatchFetchStyle.cs new file mode 100644 index 00000000000..38948ca22b1 --- /dev/null +++ b/src/NHibernate/Loader/BatchFetchStyle.cs @@ -0,0 +1,33 @@ +namespace NHibernate.Loader +{ + /// + /// Defines the style that should be used to perform batch loading. + /// + public enum BatchFetchStyle + { + /// + /// The legacy algorithm where we keep a set of pre-built batch sizes. Batches are performed + /// using the next-smaller pre-built batch size from the number of existing batchable identifiers. + ///

+ /// For example, with a batch-size setting of 32 the pre-built batch sizes would be [32, 16, 10, 9, 8, 7, .., 1]. + /// An attempt to batch load 31 identifiers would result in batches of 16, 10, and 5. + ///

+ Legacy, + + // /// + // /// Still keeps the concept of pre-built batch sizes, but uses the next-bigger batch size and pads the extra + // /// identifier placeholders. + // ///

+ // /// Using the same example of a batch-size setting of 32 the pre-built batch sizes would be the same. However, the + // /// attempt to batch load 31 identifiers would result just a single batch of size 32. The identifiers to load would + // /// be "padded" (aka, repeated) to make up the difference. + // ///

+ // Padded, + + /// + /// Dynamically builds its SQL based on the actual number of available ids. Does still limit to the batch-size + /// defined on the entity/collection + /// + Dynamic, + } +} diff --git a/src/NHibernate/Loader/Collection/AbstractBatchingCollectionInitializer.cs b/src/NHibernate/Loader/Collection/AbstractBatchingCollectionInitializer.cs new file mode 100644 index 00000000000..9cf1832d784 --- /dev/null +++ b/src/NHibernate/Loader/Collection/AbstractBatchingCollectionInitializer.cs @@ -0,0 +1,17 @@ +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + public abstract partial class AbstractBatchingCollectionInitializer : ICollectionInitializer + { + protected IQueryableCollection CollectionPersister { get; } + + protected AbstractBatchingCollectionInitializer(IQueryableCollection collectionPersister) + { + CollectionPersister = collectionPersister; + } + + public abstract void Initialize(object id, ISessionImplementor session); + } +} diff --git a/src/NHibernate/Loader/Collection/BatchingCollectionInitializer.cs b/src/NHibernate/Loader/Collection/BatchingCollectionInitializer.cs index 0df933e660a..4f7cae78d91 100644 --- a/src/NHibernate/Loader/Collection/BatchingCollectionInitializer.cs +++ b/src/NHibernate/Loader/Collection/BatchingCollectionInitializer.cs @@ -11,23 +11,27 @@ namespace NHibernate.Loader.Collection ///
/// /// - public partial class BatchingCollectionInitializer : ICollectionInitializer + public partial class BatchingCollectionInitializer : AbstractBatchingCollectionInitializer { private readonly Loader[] loaders; private readonly int[] batchSizes; - private readonly ICollectionPersister collectionPersister; - public BatchingCollectionInitializer(ICollectionPersister collectionPersister, int[] batchSizes, Loader[] loaders) + //Since 5.4 + [Obsolete("Please use ctor with IQueryableCollection collectionPersister")] + public BatchingCollectionInitializer(ICollectionPersister collectionPersister, int[] batchSizes, Loader[] loaders) : this((IQueryableCollection) collectionPersister, batchSizes, loaders) + { + } + + public BatchingCollectionInitializer(IQueryableCollection collectionPersister, int[] batchSizes, Loader[] loaders) : base(collectionPersister) { this.loaders = loaders; this.batchSizes = batchSizes; - this.collectionPersister = collectionPersister; } - public void Initialize(object id, ISessionImplementor session) + public override void Initialize(object id, ISessionImplementor session) { object[] batch = - session.PersistenceContext.BatchFetchQueue.GetCollectionBatch(collectionPersister, id, batchSizes[0]); + session.PersistenceContext.BatchFetchQueue.GetCollectionBatch(CollectionPersister, id, batchSizes[0]); for (int i = 0; i < batchSizes.Length; i++) { @@ -36,15 +40,26 @@ public void Initialize(object id, ISessionImplementor session) { object[] smallBatch = new object[smallBatchSize]; Array.Copy(batch, 0, smallBatch, 0, smallBatchSize); - loaders[i].LoadCollectionBatch(session, smallBatch, collectionPersister.KeyType); + loaders[i].LoadCollectionBatch(session, smallBatch, CollectionPersister.KeyType); return; //EARLY EXIT! } } - loaders[batchSizes.Length - 1].LoadCollection(session, id, collectionPersister.KeyType); + loaders[batchSizes.Length - 1].LoadCollection(session, id, CollectionPersister.KeyType); + } + + //Since 5.4 + [Obsolete("Please use overload with IQueryableCollection persister")] + public static ICollectionInitializer CreateBatchingOneToManyInitializer( + OneToManyPersister persister, + int maxBatchSize, + ISessionFactoryImplementor factory, + IDictionary enabledFilters) + { + return CreateBatchingOneToManyInitializer((IQueryableCollection) persister, maxBatchSize, factory, enabledFilters); } - public static ICollectionInitializer CreateBatchingOneToManyInitializer(OneToManyPersister persister, int maxBatchSize, + public static ICollectionInitializer CreateBatchingOneToManyInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) { @@ -86,4 +101,4 @@ public void Initialize(object id, ISessionImplementor session) } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Loader/Collection/BatchingCollectionInitializerBuilder.cs b/src/NHibernate/Loader/Collection/BatchingCollectionInitializerBuilder.cs new file mode 100644 index 00000000000..a465aafdbef --- /dev/null +++ b/src/NHibernate/Loader/Collection/BatchingCollectionInitializerBuilder.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + /// + /// Contract for building instances capable of performing batch-fetch loading. + /// + public abstract class BatchingCollectionInitializerBuilder + { + /// + /// Builds a batch-fetch capable ICollectionInitializer for basic and many-to-many collections (collections with + /// a dedicated collection table). + /// + /// The collection persister + /// The maximum number of keys to batch-fetch together + /// The SessionFactory + /// + /// The batch-fetch capable collection initializer + public virtual ICollectionInitializer CreateBatchingCollectionInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + if (maxBatchSize <= 1) + { + // no batching + return new BasicCollectionLoader(persister, factory, enabledFilters); + } + + return CreateRealBatchingCollectionInitializer(persister, maxBatchSize, factory, enabledFilters); + } + + protected abstract ICollectionInitializer CreateRealBatchingCollectionInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters); + + /// + /// Builds a batch-fetch capable ICollectionInitializer for one-to-many collections (collections without + /// a dedicated collection table). + /// + /// The collection persister + /// The maximum number of keys to batch-fetch together + /// The SessionFactory + /// + /// The batch-fetch capable collection initializer + public virtual ICollectionInitializer CreateBatchingOneToManyInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + if (maxBatchSize <= 1) + { + // no batching + return new OneToManyLoader(persister, factory, enabledFilters); + } + + return CreateRealBatchingOneToManyInitializer(persister, maxBatchSize, factory, enabledFilters); + } + + protected abstract ICollectionInitializer CreateRealBatchingOneToManyInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters); + } +} diff --git a/src/NHibernate/Loader/Collection/CollectionLoader.cs b/src/NHibernate/Loader/Collection/CollectionLoader.cs index 4c44cee8a1c..b5e89099f0e 100644 --- a/src/NHibernate/Loader/Collection/CollectionLoader.cs +++ b/src/NHibernate/Loader/Collection/CollectionLoader.cs @@ -30,6 +30,8 @@ public override bool IsSubselectLoadingEnabled get { return HasSubselectLoadableCollections(); } } + protected IQueryableCollection CollectionPersister => collectionPersister; + protected IType KeyType { get { return collectionPersister.KeyType; } diff --git a/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializer.cs b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializer.cs new file mode 100644 index 00000000000..80cd8301532 --- /dev/null +++ b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializer.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + internal partial class DynamicBatchingCollectionInitializer : AbstractBatchingCollectionInitializer + { + readonly int _maxBatchSize; + readonly Loader _singleKeyLoader; + readonly DynamicBatchingCollectionLoader _batchLoader; + + public DynamicBatchingCollectionInitializer(IQueryableCollection collectionPersister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(collectionPersister) + { + _maxBatchSize = maxBatchSize; + + _singleKeyLoader = collectionPersister.IsOneToMany + ? (Loader) new OneToManyLoader(collectionPersister, 1, factory, enabledFilters) + : new BasicCollectionLoader(collectionPersister, 1, factory, enabledFilters); + + _batchLoader = new DynamicBatchingCollectionLoader(collectionPersister, factory, enabledFilters); + } + + public override void Initialize(object id, ISessionImplementor session) + { + // first, figure out how many batchable ids we have... + object[] batch = session.PersistenceContext.BatchFetchQueue.GetCollectionBatch(CollectionPersister, id, _maxBatchSize); + var numberOfIds = DynamicBatchingHelper.GetIdsToLoad(batch, out var idsToLoad); + if (numberOfIds <= 1) + { + _singleKeyLoader.LoadCollection(session, id, CollectionPersister.KeyType); + return; + } + + _batchLoader.LoadCollectionBatch(session, idsToLoad, CollectionPersister.KeyType); + } + } +} diff --git a/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializerBuilder.cs b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializerBuilder.cs new file mode 100644 index 00000000000..31297b15ff8 --- /dev/null +++ b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionInitializerBuilder.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + /// + /// A BatchingCollectionInitializerBuilder that builds ICollectionInitializer instances capable of dynamically building + /// its batch-fetch SQL based on the actual number of collections keys waiting to be fetched. + /// + public partial class DynamicBatchingCollectionInitializerBuilder : BatchingCollectionInitializerBuilder + { + protected override ICollectionInitializer CreateRealBatchingCollectionInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return new DynamicBatchingCollectionInitializer(persister, maxBatchSize, factory, enabledFilters); + } + + protected override ICollectionInitializer CreateRealBatchingOneToManyInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return new DynamicBatchingCollectionInitializer(persister, maxBatchSize, factory, enabledFilters); + } + } +} diff --git a/src/NHibernate/Loader/Collection/DynamicBatchingCollectionLoader.cs b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionLoader.cs new file mode 100644 index 00000000000..bd7113607d3 --- /dev/null +++ b/src/NHibernate/Loader/Collection/DynamicBatchingCollectionLoader.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Param; +using NHibernate.Persister.Collection; +using NHibernate.SqlCommand; +using NHibernate.Util; + +namespace NHibernate.Loader.Collection +{ + internal class DynamicBatchingCollectionLoader : CollectionLoader + { + readonly string _alias; + + public DynamicBatchingCollectionLoader(IQueryableCollection collectionPersister, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(collectionPersister, factory, enabledFilters) + { + var walker = collectionPersister.IsOneToMany + ? (JoinWalker) new DynamicOneToManyJoinWalker(collectionPersister, factory, enabledFilters) + : new DynamicBasicCollectionJoinWalker(collectionPersister, factory, enabledFilters); + InitFromWalker(walker); + _alias = StringHelper.GenerateAlias(collectionPersister.Role, 0); + PostInstantiate(); + } + + private protected override SqlString TransformSql(SqlString sqlString, QueryParameters queryParameters, HashSet parameterSpecifications) + { + var columns = StringHelper.Qualify(_alias, CollectionPersister.KeyColumnNames); + return DynamicBatchingHelper.ExpandBatchIdPlaceholder(sqlString, parameterSpecifications, columns, queryParameters.PositionalParameterTypes, Factory); + } + + private class DynamicOneToManyJoinWalker : OneToManyJoinWalker + { + public DynamicOneToManyJoinWalker(IQueryableCollection collectionPersister, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(collectionPersister, 1, null, factory, enabledFilters) + { + } + + protected override SqlStringBuilder WhereString(string alias, string[] columnNames, int batchSize) + { + return DynamicBatchingHelper.BuildBatchFetchRestrictionFragment(); + } + } + + private class DynamicBasicCollectionJoinWalker : BasicCollectionJoinWalker + { + public DynamicBasicCollectionJoinWalker(IQueryableCollection collectionPersister, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(collectionPersister, 1, null, factory, enabledFilters) + { + } + + protected override SqlStringBuilder WhereString(string alias, string[] columnNames, int batchSize) + { + return DynamicBatchingHelper.BuildBatchFetchRestrictionFragment(); + } + } + } +} diff --git a/src/NHibernate/Loader/Collection/LegacyBatchingCollectionInitializerBuilder.cs b/src/NHibernate/Loader/Collection/LegacyBatchingCollectionInitializerBuilder.cs new file mode 100644 index 00000000000..8aec6aa4fdf --- /dev/null +++ b/src/NHibernate/Loader/Collection/LegacyBatchingCollectionInitializerBuilder.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Collection; + +namespace NHibernate.Loader.Collection +{ + public class LegacyBatchingCollectionInitializerBuilder : BatchingCollectionInitializerBuilder + { + protected override ICollectionInitializer CreateRealBatchingCollectionInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return BatchingCollectionInitializer.CreateBatchingCollectionInitializer(persister, maxBatchSize, factory, enabledFilters); + } + + protected override ICollectionInitializer CreateRealBatchingOneToManyInitializer(IQueryableCollection persister, int maxBatchSize, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return BatchingCollectionInitializer.CreateBatchingOneToManyInitializer(persister, maxBatchSize, factory, enabledFilters); + } + } +} diff --git a/src/NHibernate/Loader/DynamicBatchingHelper.cs b/src/NHibernate/Loader/DynamicBatchingHelper.cs new file mode 100644 index 00000000000..e39cb124fcb --- /dev/null +++ b/src/NHibernate/Loader/DynamicBatchingHelper.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using NHibernate.Criterion; +using NHibernate.Engine; +using NHibernate.Param; +using NHibernate.SqlCommand; +using NHibernate.Type; + +namespace NHibernate.Loader +{ + internal class DynamicBatchingHelper + { + private static string BatchIdPlaceholder = "$$BATCH_ID_PLACEHOLDER$$"; + + public static SqlStringBuilder BuildBatchFetchRestrictionFragment() + { + return new SqlStringBuilder(1).Add(BatchIdPlaceholder); + } + + public static SqlString ExpandBatchIdPlaceholder( + SqlString sqlString, + ISet specifications, + string[] columns, + IType[] types, + ISessionFactoryImplementor factory) + { + var parameters = GeneratePositionalParameters(specifications, types, factory); + + var wherePart = InExpression.GetSqlString(columns, types.Length, parameters, factory.Dialect); + + return sqlString.ReplaceLast(BatchIdPlaceholder, wherePart); + } + + public static List GeneratePositionalParameters( + ISet specifications, + IType[] types, + ISessionFactoryImplementor factory) + { + var parameters = new List(); + for (var i = 0; i < types.Length; i++) + { + var specification = new PositionalParameterSpecification(1, 0, i) { ExpectedType = types[i] }; + foreach (var id in specification.GetIdsForBackTrack(factory)) + { + var p = Parameter.Placeholder; + p.BackTrack = id; + parameters.Add(p); + } + + specifications.Add(specification); + } + + return parameters; + } + + public static int GetIdsToLoad(object[] batch, out object[] idsToLoad) + { + int numberOfIds = Array.IndexOf(batch, null); + if (numberOfIds < 0) + { + idsToLoad = batch; + return batch.Length; + } + + if (numberOfIds == 1) + { + idsToLoad = null; + return numberOfIds; + } + + idsToLoad = new object[numberOfIds]; + Array.Copy(batch, 0, idsToLoad, 0, numberOfIds); + return numberOfIds; + } + } +} diff --git a/src/NHibernate/Loader/Entity/AbstractBatchingEntityLoader.cs b/src/NHibernate/Loader/Entity/AbstractBatchingEntityLoader.cs new file mode 100644 index 00000000000..71b6bc4d2a5 --- /dev/null +++ b/src/NHibernate/Loader/Entity/AbstractBatchingEntityLoader.cs @@ -0,0 +1,56 @@ +using System.Collections; +using NHibernate.Engine; +using NHibernate.Persister.Entity; +using NHibernate.Type; +using NHibernate.Util; + +namespace NHibernate.Loader.Entity +{ + /// + /// The base contract for loaders capable of performing batch-fetch loading of entities using multiple primary key + /// values in the SQL WHERE clause. + /// + public abstract partial class AbstractBatchingEntityLoader : IUniqueEntityLoader + { + protected IEntityPersister Persister { get; } + + protected AbstractBatchingEntityLoader(IEntityPersister persister) + { + Persister = persister; + } + + protected virtual QueryParameters BuildQueryParameters(object id, object[] ids, object optionalObject) + { + IType[] types = new IType[ids.Length]; + ArrayHelper.Fill(types, Persister.IdentifierType); + + QueryParameters qp = new QueryParameters + { + PositionalParameterTypes = types, + PositionalParameterValues = ids, + OptionalObject = optionalObject, + OptionalEntityName = Persister.EntityName, + OptionalId = id, + }; + return qp; + } + + protected object GetObjectFromList(IList results, object id, ISessionImplementor session) + { + // get the right object from the list ... would it be easier to just call getEntity() ?? + foreach (object obj in results) + { + bool equal = Persister.IdentifierType.IsEqual(id, session.GetContextEntityIdentifier(obj), session.Factory); + + if (equal) + { + return obj; + } + } + + return null; + } + + public abstract object Load(object id, object optionalObject, ISessionImplementor session); + } +} diff --git a/src/NHibernate/Loader/Entity/AbstractEntityLoader.cs b/src/NHibernate/Loader/Entity/AbstractEntityLoader.cs index b3deffa04c8..51704026c18 100644 --- a/src/NHibernate/Loader/Entity/AbstractEntityLoader.cs +++ b/src/NHibernate/Loader/Entity/AbstractEntityLoader.cs @@ -74,7 +74,7 @@ protected virtual object Load(ISessionImplementor session, object id, object opt protected IType UniqueKeyType { get; private set; } - private IEnumerable CreateParameterSpecificationsAndAssignBackTrack(IEnumerable sqlPatameters) + private List CreateParameterSpecificationsAndAssignBackTrack(IEnumerable sqlPatameters) { var specifications = new List(); int position = 0; @@ -97,4 +97,4 @@ protected override IEnumerable GetParameterSpecificatio return parametersSpecifications ?? (parametersSpecifications = CreateParameterSpecificationsAndAssignBackTrack(SqlString.GetParameters()).ToArray()); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Loader/Entity/BatchingEntityLoader.cs b/src/NHibernate/Loader/Entity/BatchingEntityLoader.cs index e2455bdf888..ae9a2f1fc7e 100644 --- a/src/NHibernate/Loader/Entity/BatchingEntityLoader.cs +++ b/src/NHibernate/Loader/Entity/BatchingEntityLoader.cs @@ -13,41 +13,23 @@ namespace NHibernate.Loader.Entity /// SQL where clause. /// /// - public partial class BatchingEntityLoader : IUniqueEntityLoader + public partial class BatchingEntityLoader : AbstractBatchingEntityLoader { private readonly Loader[] loaders; private readonly int[] batchSizes; - private readonly IEntityPersister persister; private readonly IType idType; - public BatchingEntityLoader(IEntityPersister persister, int[] batchSizes, Loader[] loaders) + public BatchingEntityLoader(IEntityPersister persister, int[] batchSizes, Loader[] loaders) : base(persister) { this.batchSizes = batchSizes; this.loaders = loaders; - this.persister = persister; idType = persister.IdentifierType; } - private object GetObjectFromList(IList results, object id, ISessionImplementor session) - { - // get the right object from the list ... would it be easier to just call getEntity() ?? - foreach (object obj in results) - { - bool equal = idType.IsEqual(id, session.GetContextEntityIdentifier(obj), session.Factory); - - if (equal) - { - return obj; - } - } - - return null; - } - - public object Load(object id, object optionalObject, ISessionImplementor session) + public override object Load(object id, object optionalObject, ISessionImplementor session) { object[] batch = - session.PersistenceContext.BatchFetchQueue.GetEntityBatch(persister, id, batchSizes[0]); + session.PersistenceContext.BatchFetchQueue.GetEntityBatch(Persister, id, batchSizes[0]); for (int i = 0; i < batchSizes.Length - 1; i++) { @@ -58,7 +40,7 @@ public object Load(object id, object optionalObject, ISessionImplementor session Array.Copy(batch, 0, smallBatch, 0, smallBatchSize); IList results = - loaders[i].LoadEntityBatch(session, smallBatch, idType, optionalObject, persister.EntityName, id, persister); + loaders[i].LoadEntityBatch(session, smallBatch, idType, optionalObject, Persister.EntityName, id, Persister); return GetObjectFromList(results, id, session); //EARLY EXIT } @@ -87,4 +69,4 @@ public object Load(object id, object optionalObject, ISessionImplementor session } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Loader/Entity/BatchingEntityLoaderBuilder.cs b/src/NHibernate/Loader/Entity/BatchingEntityLoaderBuilder.cs new file mode 100644 index 00000000000..cb8c282b3e7 --- /dev/null +++ b/src/NHibernate/Loader/Entity/BatchingEntityLoaderBuilder.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Entity; + +namespace NHibernate.Loader.Entity +{ + /// + /// The contract for building capable of performing batch-fetch loading. + /// + public abstract class BatchingEntityLoaderBuilder + { + /// + /// Builds a batch-fetch capable loader based on the given persister, lock-mode, etc. + /// + /// The entity persister + /// The maximum number of ids to batch-fetch at once + /// The lock mode + /// The SessionFactory + /// + /// The loader. + public virtual IUniqueEntityLoader BuildLoader(IOuterJoinLoadable persister, int batchSize, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + if (batchSize <= 1) + { + // no batching + return new EntityLoader(persister, lockMode, factory, enabledFilters); + } + + return BuildBatchingLoader(persister, batchSize, lockMode, factory, enabledFilters); + } + + protected abstract IUniqueEntityLoader BuildBatchingLoader(IOuterJoinLoadable persister, int batchSize, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters); + } +} diff --git a/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoader.cs b/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoader.cs new file mode 100644 index 00000000000..d3f6e34fb60 --- /dev/null +++ b/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoader.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Entity; + +namespace NHibernate.Loader.Entity +{ + internal partial class DynamicBatchingEntityLoader : AbstractBatchingEntityLoader + { + readonly int _maxBatchSize; + readonly IUniqueEntityLoader _singleKeyLoader; + readonly DynamicEntityLoader _dynamicEntityLoader; + + public DynamicBatchingEntityLoader(IOuterJoinLoadable persister, int maxBatchSize, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(persister) + { + this._maxBatchSize = maxBatchSize; + this._singleKeyLoader = new EntityLoader(persister, 1, lockMode, factory, enabledFilters); + this._dynamicEntityLoader = new DynamicEntityLoader(persister, lockMode, factory, enabledFilters); + } + + public override object Load(object id, object optionalObject, ISessionImplementor session) + { + object[] batch = session.PersistenceContext.BatchFetchQueue.GetEntityBatch(Persister, id, _maxBatchSize); + + var numberOfIds = DynamicBatchingHelper.GetIdsToLoad(batch, out var idsToLoad); + if (numberOfIds <= 1) + { + return _singleKeyLoader.Load(id, optionalObject, session); + } + + QueryParameters qp = BuildQueryParameters(id, idsToLoad, optionalObject); + IList results = _dynamicEntityLoader.DoEntityBatchFetch(session, qp); + return GetObjectFromList(results, id, session); + } + } +} diff --git a/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoaderBuilder.cs b/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoaderBuilder.cs new file mode 100644 index 00000000000..e516c1df496 --- /dev/null +++ b/src/NHibernate/Loader/Entity/DynamicBatchingEntityLoaderBuilder.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Entity; + +namespace NHibernate.Loader.Entity +{ + /// + /// Builds instances capable of dynamically building + /// its batch-fetch SQL based on the actual number of entity ids waiting to be fetched. + /// + public class DynamicBatchingEntityLoaderBuilder : BatchingEntityLoaderBuilder + { + protected override IUniqueEntityLoader BuildBatchingLoader(IOuterJoinLoadable persister, int batchSize, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return new DynamicBatchingEntityLoader(persister, batchSize, lockMode, factory, enabledFilters); + } + } +} diff --git a/src/NHibernate/Loader/Entity/DynamicEntityLoader.cs b/src/NHibernate/Loader/Entity/DynamicEntityLoader.cs new file mode 100644 index 00000000000..f3c0e374260 --- /dev/null +++ b/src/NHibernate/Loader/Entity/DynamicEntityLoader.cs @@ -0,0 +1,51 @@ +using System.Collections; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Param; +using NHibernate.Persister.Entity; +using NHibernate.SqlCommand; +using NHibernate.Util; + +namespace NHibernate.Loader.Entity +{ + internal partial class DynamicEntityLoader : EntityLoader + { + readonly string _alias; + + public DynamicEntityLoader(IOuterJoinLoadable persister, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(persister, lockMode, factory, enabledFilters) + { + var walker = new DynamicEntityJoinWalker(persister, persister.IdentifierColumnNames, lockMode, factory, enabledFilters); + + InitFromWalker(walker); + this._alias = walker.Alias; + PostInstantiate(); + } + + protected override bool IsSingleRowLoader => false; + + public virtual IList DoEntityBatchFetch(ISessionImplementor session, QueryParameters queryParameters) + { + return LoadEntityBatch(session, persister, queryParameters); + } + + private protected override SqlString TransformSql(SqlString sqlString, QueryParameters queryParameters, HashSet parameterSpecifications) + { + var columns = StringHelper.Qualify(_alias, persister.KeyColumnNames); + return DynamicBatchingHelper.ExpandBatchIdPlaceholder(sqlString, parameterSpecifications, columns, queryParameters.PositionalParameterTypes, Factory); + } + + class DynamicEntityJoinWalker : EntityJoinWalker + { + public DynamicEntityJoinWalker(IOuterJoinLoadable persister, string[] identifierColumnNames, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) : base(persister, identifierColumnNames, 1, lockMode, factory, enabledFilters) + { + } + + protected override SqlStringBuilder WhereString(string alias, string[] columnNames, int batchSize) + { + return DynamicBatchingHelper.BuildBatchFetchRestrictionFragment(); + } + + public new string Alias => base.Alias; + } + } +} diff --git a/src/NHibernate/Loader/Entity/LegacyBatchingEntityLoaderBuilder.cs b/src/NHibernate/Loader/Entity/LegacyBatchingEntityLoaderBuilder.cs new file mode 100644 index 00000000000..e77176c8301 --- /dev/null +++ b/src/NHibernate/Loader/Entity/LegacyBatchingEntityLoaderBuilder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Persister.Entity; + +namespace NHibernate.Loader.Entity +{ + /// + /// Default batching builder. See + /// + public class LegacyBatchingEntityLoaderBuilder : BatchingEntityLoaderBuilder + { + protected override IUniqueEntityLoader BuildBatchingLoader(IOuterJoinLoadable persister, int batchSize, LockMode lockMode, ISessionFactoryImplementor factory, IDictionary enabledFilters) + { + return BatchingEntityLoader.CreateBatchingEntityLoader(persister, batchSize, lockMode, factory, enabledFilters); + } + } +} diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs index 0407add190d..bc51a1c593f 100644 --- a/src/NHibernate/Loader/JoinWalker.cs +++ b/src/NHibernate/Loader/JoinWalker.cs @@ -984,7 +984,7 @@ protected virtual string GenerateAliasForColumn(string rootAlias, string column) /// /// Render the where condition for a (batch) load by identifier / collection key /// - protected SqlStringBuilder WhereString(string alias, string[] columnNames, int batchSize) + protected virtual SqlStringBuilder WhereString(string alias, string[] columnNames, int batchSize) { if (columnNames.Length == 1) { diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 6a262b8a6ad..6a74b42032b 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1673,20 +1673,30 @@ private ColumnNameCache RetreiveColumnNameToIndexCache(DbDataReader rs) object optionalObject, string optionalEntityName, object optionalId, IEntityPersister persister) { + IType[] types = new IType[ids.Length]; + ArrayHelper.Fill(types, idType); + var queryParameters = new QueryParameters( + types, + ids, + optionalObject, + optionalEntityName, + optionalId); + return LoadEntityBatch(session, persister, queryParameters); + } + + protected internal IList LoadEntityBatch(ISessionImplementor session, IEntityPersister persister, QueryParameters queryParameters) + { + var ids = queryParameters.PositionalParameterValues; if (Log.IsDebugEnabled()) { Log.Debug("batch loading entity: {0}", MessageHelper.InfoString(persister, ids, Factory)); } - IType[] types = new IType[ids.Length]; - ArrayHelper.Fill(types, idType); - IList result; try { - result = - DoQueryAndInitializeNonLazyCollections(session, - new QueryParameters(types, ids, optionalObject, optionalEntityName, - optionalId), false); + var results = DoQueryAndInitializeNonLazyCollections(session, queryParameters, false); + Log.Debug("done entity batch load"); + return results; } catch (HibernateException) { @@ -1699,9 +1709,6 @@ private ColumnNameCache RetreiveColumnNameToIndexCache(DbDataReader rs) + MessageHelper.InfoString(persister, ids, Factory), SqlString); // NH: Hibernate3 passes EntityPersisters[0] instead of persister, I think it's wrong. } - - Log.Debug("done entity batch load"); - return result; } /// @@ -2041,6 +2048,7 @@ public virtual ISqlCommand CreateSqlCommand(QueryParameters queryParameters, ISe var parameterSpecs = new HashSet(GetParameterSpecifications()); SqlString sqlString = SqlString.Copy(); + sqlString = TransformSql(sqlString, queryParameters, parameterSpecs); // dynamic-filter parameters: during the createion of the SqlString of allLoader implementation, filters can be added as SQL_TOKEN/string for this reason we have to re-parse the SQL. sqlString = ExpandDynamicFilterParameters(sqlString, parameterSpecs, session); AdjustQueryParametersForSubSelectFetching(sqlString, parameterSpecs, queryParameters); @@ -2057,6 +2065,11 @@ public virtual ISqlCommand CreateSqlCommand(QueryParameters queryParameters, ISe return new SqlCommandImpl(sqlString, parameterSpecs, queryParameters, session.Factory); } + private virtual protected SqlString TransformSql(SqlString sqlString, QueryParameters queryParameters, HashSet parameterSpecifications) + { + return sqlString; + } + protected virtual void ResetEffectiveExpectedType(IEnumerable parameterSpecs, QueryParameters queryParameters) { // Have to be overridden just by those loaders that can't infer the type during the parse process diff --git a/src/NHibernate/Persister/Collection/BasicCollectionPersister.cs b/src/NHibernate/Persister/Collection/BasicCollectionPersister.cs index 18ca77da8ac..59cb5037335 100644 --- a/src/NHibernate/Persister/Collection/BasicCollectionPersister.cs +++ b/src/NHibernate/Persister/Collection/BasicCollectionPersister.cs @@ -312,7 +312,7 @@ public override string SelectFragment(IJoinable rhs, string rhsAlias, string lhs /// protected override ICollectionInitializer CreateCollectionInitializer(IDictionary enabledFilters) { - return BatchingCollectionInitializer.CreateBatchingCollectionInitializer(this, batchSize, Factory, enabledFilters); + return Factory.Settings.BatchingCollectionInitializationBuilder.CreateBatchingCollectionInitializer(this, batchSize, Factory, enabledFilters); } public override SqlString FromJoinFragment(string alias, bool innerJoin, bool includeSubclasses) diff --git a/src/NHibernate/Persister/Collection/OneToManyPersister.cs b/src/NHibernate/Persister/Collection/OneToManyPersister.cs index a501de1b8c0..8893be12f6a 100644 --- a/src/NHibernate/Persister/Collection/OneToManyPersister.cs +++ b/src/NHibernate/Persister/Collection/OneToManyPersister.cs @@ -351,7 +351,7 @@ protected override SelectFragment GenerateSelectFragment(string alias, string co /// protected override ICollectionInitializer CreateCollectionInitializer(IDictionary enabledFilters) { - return BatchingCollectionInitializer.CreateBatchingOneToManyInitializer(this, batchSize, Factory, enabledFilters); + return Factory.Settings.BatchingCollectionInitializationBuilder.CreateBatchingOneToManyInitializer(this, batchSize, Factory, enabledFilters); } public override SqlString FromJoinFragment(string alias, bool innerJoin, bool includeSubclasses) diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index 409808945c9..ccad078e322 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -2508,7 +2508,7 @@ private void InitPropertyPaths(IMapping mapping) protected IUniqueEntityLoader CreateEntityLoader(LockMode lockMode, IDictionary enabledFilters) { //TODO: disable batch loading if lockMode > READ? - return BatchingEntityLoader.CreateBatchingEntityLoader(this, batchSize, lockMode, Factory, enabledFilters); + return Factory.Settings.BatchingEntityLoaderBuilder.BuildLoader(this, batchSize, lockMode, Factory, enabledFilters); } protected IUniqueEntityLoader CreateEntityLoader(LockMode lockMode) diff --git a/src/NHibernate/SqlCommand/SqlString.cs b/src/NHibernate/SqlCommand/SqlString.cs index 793453a5d6a..311f8a06ef0 100644 --- a/src/NHibernate/SqlCommand/SqlString.cs +++ b/src/NHibernate/SqlCommand/SqlString.cs @@ -502,6 +502,11 @@ public int LastIndexOfCaseInsensitive(string text) return LastIndexOf(text, 0, _length, StringComparison.InvariantCultureIgnoreCase); } + internal int LastIndexOf(string text, StringComparison comparison) + { + return LastIndexOf(text, 0, _length, comparison); + } + /// /// Returns the index of the first occurrence of , case-insensitive. /// @@ -1089,5 +1094,14 @@ public override string ToString() return Content; } } + + internal SqlString ReplaceLast(string from, SqlString to) + { + var index = LastIndexOf(from, StringComparison.Ordinal); + return new SqlString( + Substring(0, index), + to, + Substring(index + from.Length)); + } } } diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd index 4d6022543af..cd0077fe010 100644 --- a/src/NHibernate/nhibernate-configuration.xsd +++ b/src/NHibernate/nhibernate-configuration.xsd @@ -170,6 +170,7 @@ +