diff --git a/src/NHibernate.Test/NHSpecificTest/NH3489/Department.cs b/src/NHibernate.Test/NHSpecificTest/NH3489/Department.cs new file mode 100644 index 00000000000..329d9aa0cb4 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3489/Department.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3489 +{ + public class Department + { + public Department() + { + Orders = new HashSet(); + } + + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual ISet Orders { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3489/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3489/Fixture.cs new file mode 100644 index 00000000000..6a892148cf2 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3489/Fixture.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.NHSpecificTest.NH3489 +{ + [TestFixture] + [Ignore("Only run to test performance.")] + public class Fixture : TestCaseMappingByCode + { + private const int batchSize = 900; + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(cm => + { + cm.Id(x => x.Id, m => m.Generator(Generators.HighLow)); + cm.Table("Orders"); + cm.Property(x => x.Name); + cm.Set(x => x.Departments, m => + { + m.Table("Orders_Departments"); + m.Key(k => + { + k.Column("OrderId"); + k.NotNullable(true); + }); + m.BatchSize(batchSize); + m.Cascade(Mapping.ByCode.Cascade.All); + }, r => r.ManyToMany(c => c.Column("DepartmentId"))); + }); + + mapper.Class(cm => + { + cm.Id(x => x.Id, m => m.Generator(Generators.HighLow)); + cm.Table("Departments"); + cm.Set(x => x.Orders, m => + { + m.Table("Orders_Departments"); + m.Key(k => + { + k.Column("DepartmentId"); + k.NotNullable(true); + }); + m.BatchSize(batchSize); + m.Cascade(Mapping.ByCode.Cascade.All); + m.Inverse(true); + }, r => r.ManyToMany(c => c.Column("OrderId"))); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(Environment.BatchSize, batchSize.ToString(CultureInfo.InvariantCulture)); + } + + protected override void OnSetUp() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + var department1 = new Department {Name = "Dep 1"}; + session.Save(department1); + var department2 = new Department {Name = "Dep 2"}; + session.Save(department2); + + for (int i = 0; i < 10000; i++) + { + var order = new Order {Name = "Order " + (i + 1)}; + order.Departments.Add(department1); + order.Departments.Add(department2); + session.Save(order); + } + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override string CacheConcurrencyStrategy + { + get { return null; } + } + + [Test] + public void PerformanceTest() + { + Stopwatch stopwatch = Stopwatch.StartNew(); + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + IList orders = session.QueryOver().List(); + foreach (Order order in orders) + order.Departments.ToList(); + } + stopwatch.Stop(); + Console.WriteLine(stopwatch.Elapsed); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3489/Order.cs b/src/NHibernate.Test/NHSpecificTest/NH3489/Order.cs new file mode 100644 index 00000000000..7896bc42d1b --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3489/Order.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3489 +{ + public class Order + { + public Order() + { + Departments = new HashSet(); + } + + public virtual int Id { get; set; } + public virtual string Name { get; set; } + + public virtual ISet Departments { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index c60b89ab659..c588bfa88e8 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -719,6 +719,9 @@ + + + diff --git a/src/NHibernate/NHibernate.csproj b/src/NHibernate/NHibernate.csproj index 54fa348f4e2..338853eca0a 100644 --- a/src/NHibernate/NHibernate.csproj +++ b/src/NHibernate/NHibernate.csproj @@ -540,6 +540,7 @@ + diff --git a/src/NHibernate/Param/ParametersBackTrackExtensions.cs b/src/NHibernate/Param/ParametersBackTrackExtensions.cs index 64e31dbf6ce..1d0459a8eb4 100644 --- a/src/NHibernate/Param/ParametersBackTrackExtensions.cs +++ b/src/NHibernate/Param/ParametersBackTrackExtensions.cs @@ -11,16 +11,28 @@ public static class ParametersBackTrackExtensions { public static IEnumerable GetEffectiveParameterLocations(this IList sqlParameters, string backTrackId) { + var cacheParameters = sqlParameters as BackTrackCacheParameterList; + return cacheParameters != null + ? cacheParameters.GetEffectiveParameterLocations(backTrackId) + : GetEffectiveParameterLocationsSlow(sqlParameters, backTrackId); + } + + private static IEnumerable GetEffectiveParameterLocationsSlow(IList sqlParameters, string backTrackId) { for (int i = 0; i < sqlParameters.Count; i++) { if (backTrackId.Equals(sqlParameters[i].BackTrack)) - { yield return i; - } } } - public static SqlType[] GetQueryParameterTypes(this IEnumerable parameterSpecs, List sqlQueryParametersList, ISessionFactoryImplementor factory) + internal static BackTrackCacheParameterList ToBackTrackCacheParameterList(this IEnumerable sqlParameters) { + var list = new BackTrackCacheParameterList(); + foreach (Parameter sqlParameter in sqlParameters) + list.Add(sqlParameter); + return list; + } + + public static SqlType[] GetQueryParameterTypes(this IEnumerable parameterSpecs, IList sqlQueryParametersList, ISessionFactoryImplementor factory) { // NOTE: if you have a NullReferenceException probably is because the IParameterSpecification does not have the ExpectedType; use ResetEffectiveExpectedType before call this method. diff --git a/src/NHibernate/SqlCommand/BackTrackCacheParameterList.cs b/src/NHibernate/SqlCommand/BackTrackCacheParameterList.cs new file mode 100644 index 00000000000..7a2f0451f41 --- /dev/null +++ b/src/NHibernate/SqlCommand/BackTrackCacheParameterList.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using NHibernate.Util; + +namespace NHibernate.SqlCommand +{ + /// + /// A list of that maintains a cache of backtrace positions for performance purpose. + /// See https://nhibernate.jira.com/browse/NH-3489. + /// + internal class BackTrackCacheParameterList : Collection + { + private Dictionary> _indexesByBackTrace; + + private void AddIndex(Parameter parameter, int index) + { + var backTrack = parameter.BackTrack as string; + if (backTrack == null) + return; + + SortedSet indexes; + if (!_indexesByBackTrace.TryGetValue(backTrack, out indexes)) + { + indexes = new SortedSet(); + _indexesByBackTrace.Add(backTrack, indexes); + } + indexes.Add(index); + } + + private void RemoveIndexes(Parameter parameter) + { + var backTrack = parameter.BackTrack as string; + if (backTrack != null) + _indexesByBackTrace.Remove(backTrack); + } + + private Dictionary> BuildBackTrackCache() + { + var indexesByBackTrace = new Dictionary>(); + IList parameters = Items; + for (int i = 0; i < parameters.Count; i++) + { + var backTrace = parameters[i].BackTrack as string; + if (backTrace != null) + { + SortedSet locations; + if (!indexesByBackTrace.TryGetValue(backTrace, out locations)) + { + locations = new SortedSet(); + indexesByBackTrace.Add(backTrace, locations); + } + locations.Add(i); + } + } + return indexesByBackTrace; + } + + protected override void InsertItem(int index, Parameter item) + { + base.InsertItem(index, item); + if (_indexesByBackTrace != null) + AddIndex(item, index); + } + + protected override void RemoveItem(int index) + { + Parameter oldItem = Items[index]; + base.RemoveItem(index); + if (_indexesByBackTrace != null) + RemoveIndexes(oldItem); + } + + protected override void SetItem(int index, Parameter item) + { + Parameter oldItem = Items[index]; + base.SetItem(index, item); + if (_indexesByBackTrace != null) + { + RemoveIndexes(oldItem); + AddIndex(item, index); + } + } + + protected override void ClearItems() + { + base.ClearItems(); + if (_indexesByBackTrace != null) + _indexesByBackTrace.Clear(); + } + + public IEnumerable GetEffectiveParameterLocations(string backTrace) + { + if (backTrace != null) + { + if (_indexesByBackTrace == null) + _indexesByBackTrace = BuildBackTrackCache(); + SortedSet indexes; + if (_indexesByBackTrace.TryGetValue(backTrace, out indexes)) + return indexes; + } + return ArrayHelper.EmptyIntArray; + } + } +} \ No newline at end of file diff --git a/src/NHibernate/SqlCommand/SqlCommandImpl.cs b/src/NHibernate/SqlCommand/SqlCommandImpl.cs index 69546a00bbe..f3038f6efec 100644 --- a/src/NHibernate/SqlCommand/SqlCommandImpl.cs +++ b/src/NHibernate/SqlCommand/SqlCommandImpl.cs @@ -60,7 +60,7 @@ public class SqlCommandImpl : ISqlCommand private readonly QueryParameters queryParameters; private readonly ISessionFactoryImplementor factory; private SqlType[] parameterTypes; - List sqlQueryParametersList; + IList sqlQueryParametersList; public SqlCommandImpl(SqlString query, ICollection specifications, QueryParameters queryParameters, ISessionFactoryImplementor factory) { @@ -70,9 +70,9 @@ public SqlCommandImpl(SqlString query, ICollection spec this.factory = factory; } - public List SqlQueryParametersList + public IList SqlQueryParametersList { - get { return sqlQueryParametersList ?? (sqlQueryParametersList = query.GetParameters().ToList()); } + get { return sqlQueryParametersList ?? (sqlQueryParametersList = query.GetParameters().ToBackTrackCacheParameterList()); } } public SqlType[] ParameterTypes @@ -103,6 +103,7 @@ public void ResetParametersIndexesForTheCommand(int singleSqlParametersOffset) { throw new AssertionFailure("singleSqlParametersOffset < 0 - this indicate a bug in NHibernate "); } + // due to IType.NullSafeSet(System.Data.IDbCommand , object, int, ISessionImplementor) the SqlType[] is supposed to be in a certain sequence. // this mean that found the first location of a parameter for the IType span, the others are in secuence foreach (IParameterSpecification specification in Specifications) @@ -111,7 +112,7 @@ public void ResetParametersIndexesForTheCommand(int singleSqlParametersOffset) int[] effectiveParameterLocations = SqlQueryParametersList.GetEffectiveParameterLocations(firstParameterId).ToArray(); if (effectiveParameterLocations.Length > 0) // Parameters previously present might have been removed from the SQL at a later point. { - int firstParamNameIndex = effectiveParameterLocations.First() + singleSqlParametersOffset; + int firstParamNameIndex = effectiveParameterLocations[0] + singleSqlParametersOffset; foreach (int location in effectiveParameterLocations) { int parameterSpan = specification.ExpectedType.GetColumnSpan(factory);