diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 382d07d44c9..f9f2f990ce3 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -26,6 +26,9 @@ - conversion: Ignore name: InitializeLazyPropertiesFromCache containingTypeName: AbstractEntityPersister + - conversion: Ignore + name: GetElementOwnerPropertyValue + containingTypeName: AbstractCollectionPersister - conversion: Ignore name: Invoke containingTypeName: BasicLazyInitializer diff --git a/src/NHibernate.Test/Async/CollectionCompositeKey/Fixture.cs b/src/NHibernate.Test/Async/CollectionCompositeKey/Fixture.cs new file mode 100644 index 00000000000..7641c06c173 --- /dev/null +++ b/src/NHibernate.Test/Async/CollectionCompositeKey/Fixture.cs @@ -0,0 +1,217 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using NHibernate.Event; +using NHibernate.Persister.Collection; +using NUnit.Framework; +using NUnit.Framework.Constraints; + +namespace NHibernate.Test.CollectionCompositeKey +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class FixtureAsync : TestCase + { + private readonly Parent _parentId = new Parent("1", 1); + private int _currentChildId = 1; + private int _currentGrandChildId = 1; + + protected override string[] Mappings => new [] { "CollectionCompositeKey.CollectionOwner.hbm.xml" }; + + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string CacheConcurrencyStrategy => null; + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var parent = CreateParent(_parentId.Code, _parentId.Number); + + s.Save(parent); + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from GrandChild"); + session.Delete("from Child"); + session.Delete("from Parent"); + tx.Commit(); + } + base.OnTearDown(); + } + + [TestCase(nameof(Parent.Children), true, true)] + [TestCase(nameof(Parent.ChildrenByForeignKeys), true, false)] + [TestCase(nameof(Parent.ChildrenByComponent), true, true)] + [TestCase(nameof(Parent.ChildrenByComponentForeignKeys), true, false)] + [TestCase(nameof(Parent.ChildrenNoProperties), false, false)] + [TestCase(nameof(Parent.ChildrenByUniqueKey), true, true)] + [TestCase(nameof(Parent.ChildrenByUniqueKeyNoManyToOne), true, false)] + [TestCase(nameof(Parent.ChildrenByCompositeUniqueKey), true, true)] + [TestCase(nameof(Parent.ChildrenByCompositeUniqueKeyNoManyToOne), true, false)] + public Task TestGetElementOwnerForChildCollectionsAsync(string collectionProperty, bool propertyExists, bool hasManyToOne, CancellationToken cancellationToken = default(CancellationToken)) + { + return TestGetElementOwnerForChildCollectionsAsync(collectionProperty, propertyExists, hasManyToOne, (parent, child) => child.Id, cancellationToken); + } + + [TestCase(nameof(Parent.GrandChildren), true, true)] + public Task TestGetElementOwnerForGrandChildCollectionsAsync(string collectionProperty, bool propertyExists, bool hasManyToOne, CancellationToken cancellationToken = default(CancellationToken)) + { + return TestGetElementOwnerForChildCollectionsAsync(collectionProperty, propertyExists, hasManyToOne, GetGrandChildId, cancellationToken); + } + + private static object GetGrandChildId(Parent parent, GrandChild child) + { + if (parent == null) + { + return null; // Not supported + } + + child.GrandParent = parent; + return child; + } + + private async Task TestGetElementOwnerForChildCollectionsAsync(string collectionProperty, bool propertyExists, bool hasManyToOne, Func getChildId, CancellationToken cancellationToken = default(CancellationToken)) + where TChild : class + { + var persister = Sfi.GetEntityPersister(typeof(Parent).FullName); + var collPersister = GetCollectionPersister(collectionProperty); + TChild firstChild = null; + + var propertyIndex = -1; + for (var i = 0; i < persister.PropertyNames.Length; i++) + { + if (persister.PropertyNames[i] == collectionProperty) + { + propertyIndex = i; + break; + } + } + + Assert.That(propertyIndex, Is.Not.EqualTo(-1)); + + // Test when collection is loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = await (s.GetAsync(_parentId, cancellationToken)); + Assert.That(parent, Is.Not.Null); + + var collection = (IList) persister.GetPropertyValue(parent, propertyIndex); + foreach (var child in collection) + { + if (firstChild == null) + { + firstChild = child; + } + + Assert.That(collPersister.GetElementOwner(child, s), propertyExists ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + } + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(firstChild, Is.Not.Null); + + // Test when collection is not loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = await (s.GetAsync(_parentId, cancellationToken)); + var child = await (s.GetAsync(getChildId(parent, firstChild), cancellationToken)); + Assert.That(parent, Is.Not.Null); + Assert.That(child, Is.Not.Null); + + Assert.That(collPersister.GetElementOwner(child, s), propertyExists ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + + await (tx.CommitAsync(cancellationToken)); + } + + // Test when only the child is loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var id = getChildId(null, firstChild); + if (id != null) + { + var child = await (s.GetAsync(id, cancellationToken)); + Assert.That(child, Is.Not.Null); + + Assert.That(collPersister.GetElementOwner(child, s), hasManyToOne ? Is.InstanceOf() : (IResolveConstraint) Is.Null); + } + + await (tx.CommitAsync(cancellationToken)); + } + + // Test transient + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = CreateParent("2", 2); + var collection = (IList) persister.GetPropertyValue(parent, propertyIndex); + + foreach (var child in collection) + { + Assert.That(collPersister.GetElementOwner(child, s), hasManyToOne ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + } + + await (tx.CommitAsync(cancellationToken)); + } + } + + private AbstractCollectionPersister GetCollectionPersister(string collectionProperty) + { + return (AbstractCollectionPersister) Sfi.GetCollectionPersister($"{typeof(Parent).FullName}.{collectionProperty}"); + } + + private Parent CreateParent(string code, int number) + { + var parent = new Parent(code, number) + { + Name = $"parent{number}", + ReferenceCode = code, + ReferenceNumber = number + }; + + parent.Children.Add(new Child(_currentChildId++, "child", parent) { Parent = parent }); + parent.ChildrenByForeignKeys.Add(new Child(_currentChildId++, "childFk", parent) { ParentNumber = parent.Number, ParentCode = parent.Code }); + parent.ChildrenByComponent.Add(new Child(_currentChildId++, "childCo", parent) { Component = new ChildComponent { Parent = parent } }); + parent.ChildrenByComponentForeignKeys.Add( + new Child(_currentChildId++, "childCoFk", parent) + { + Component = new ChildComponent { ParentNumber = parent.Number, ParentCode = parent.Code } + }); + parent.ChildrenNoProperties.Add(new Child(_currentChildId++, "childNp", parent)); + parent.ChildrenByUniqueKey.Add(new Child(_currentChildId++, "childUk", parent) { ParentByName = parent }); + parent.ChildrenByUniqueKeyNoManyToOne.Add(new Child(_currentChildId++, "childUkFk", parent) { ParentName = parent.Name }); + parent.ChildrenByCompositeUniqueKey.Add(new Child(_currentChildId++, "childCoUk", parent) { ParentByReference = parent }); + parent.ChildrenByCompositeUniqueKeyNoManyToOne.Add( + new Child(_currentChildId++, "childCoUkFk", parent) + { + ParentReferenceCode = parent.ReferenceCode, + ParentReferenceNumber = parent.ReferenceNumber + }); + + parent.GrandChildren.Add(new GrandChild(_currentGrandChildId, "grandChild", parent)); + + return parent; + } + } +} diff --git a/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs b/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs index 4dd7e85fced..d338404834a 100644 --- a/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs +++ b/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs @@ -8,14 +8,19 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using NHibernate.Cfg; +using NHibernate.Id; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace NHibernate.Test.Extralazy { using System.Threading.Tasks; + using System.Threading; [TestFixture] public class ExtraLazyFixtureAsync : TestCase { @@ -34,13 +39,2876 @@ protected override string CacheConcurrencyStrategy get { return null; } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Cfg.Environment.GenerateStatistics, "true"); + } + protected override void OnTearDown() { using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - s.Delete("from System.Object"); - t.Commit(); + s.Delete("from System.Object"); + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapAddChildSaveChangeParentAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + await (s.PersistAsync(gavin, cancellationToken)); + await (s.PersistAsync(turin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new UserSetting($"g{i}", $"data{i}", gavin); + gavinItems.Add(item); + gavin.Settings.Add(item.Name, item); + + item = new UserSetting($"t{i}", $"data{i}", turin); + turin.Settings.Add(item.Name, item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new UserSetting($"c{i}", $"data{i}", gavin); + await (s.SaveAsync(item, cancellationToken)); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + gavin.Settings.Add(gavinItems[i].Name, gavinItems[i]); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new UserSetting($"c{i}", $"data{i}", gavin); + gavin.Settings.Add(item.Name, item); + gavinItems.Add(item); + await (s.SaveAsync(item, cancellationToken)); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Settings.Remove(item.Name); + + item.Owner = turin; + turin.Settings.Add(item.Name, item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Settings.Remove(item.Name); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + await (s.DeleteAsync(item, cancellationToken)); + } + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListInsertChildSaveChangeParentAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + await (s.PersistAsync(gavin, cancellationToken)); + await (s.PersistAsync(turin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + + item = new Company($"t{i}", i, turin); + turin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + await (s.SaveAsync(item, cancellationToken)); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + if (i % 2 != 0) + { + gavin.Companies.Insert(i, gavinItems[i]); + } + else + { + gavin.Companies.Add(gavinItems[i]); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + if (i % 2 != 0) + { + gavin.Companies.Insert(i, item); + } + else + { + gavin.Companies.Add(item); + } + + gavinItems.Add(item); + await (s.SaveAsync(item, cancellationToken)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Companies.RemoveAt(5); + + item.Owner = turin; + turin.Companies.Insert(item.ListIndex, item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Companies.RemoveAt(5); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + await (s.DeleteAsync(item, cancellationToken)); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListClearChildSaveChangeParentAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + await (s.PersistAsync(gavin, cancellationToken)); + await (s.PersistAsync(turin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"g{i}", i, gavin); + gavin.CreditCards.Add(item); + + item = new CreditCard($"t{i}", i, turin); + turin.CreditCards.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + Sfi.Statistics.Clear(); + gavin.CreditCards.Clear(); + turin.CreditCards.Clear(); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(0)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + // Save credit cards and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c2{i}", i, gavin); + await (s.SaveAsync(item, cancellationToken)); + gavinItems.Add(item); + } + + for (var i = 0; i < 5; i++) + { + gavin.CreditCards.Add(gavinItems[i]); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + + // Add credit cards to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c2{i}", i, gavin); + Assert.That(((IList) gavin.CreditCards).Add(item), Is.EqualTo(i)); + gavinItems.Add(item); + await (s.SaveAsync(item, cancellationToken)); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Take(5)) + { + gavin.CreditCards.Remove(item); + + item.Owner = turin; + item.ListIndex += 5; + turin.CreditCards.Add(item); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + if (initialize) + { + using (var e = gavin.CreditCards.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.CreditCards.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.True); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(10)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListAddChildSaveChangeParentAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + await (s.PersistAsync(gavin, cancellationToken)); + await (s.PersistAsync(turin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + + item = new Company($"t{i}", i, turin); + turin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + await (s.SaveAsync(item, cancellationToken)); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + gavin.Companies.Add(gavinItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList) gavin.Companies).Add(item), Is.EqualTo(i)); + gavinItems.Add(item); + await (s.SaveAsync(item, cancellationToken)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Companies.Remove(item); + + item.Owner = turin; + turin.Companies.Add(item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Companies.Remove(item); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + await (s.DeleteAsync(item, cancellationToken)); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + turin = await (s.GetAsync("turin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListAddChildSaveAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + await (s.SaveAsync(item, cancellationToken)); + gavinItems.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(IsNativeIdentityGenerator ? 10 : 5)); + + for (var i = 5; i < 10; i++) + { + gavin.Companies.Add(gavinItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList) gavinItems).Add(item), Is.EqualTo(i)); + gavin.Companies.Add(item); + await (s.SaveAsync(item, cancellationToken)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListAddAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test adding companies with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test adding companies with IList interface + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList)addedItems).Add(item), Is.EqualTo(i)); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + + // Check existance of added companies + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task ListAddDuplicatedAsync(bool initialize, bool flush, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Add(addedItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (flush) + { + await (s.FlushAsync(cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListInsertAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test inserting companies at the start + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(0, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test inserting companies at the end + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try insert invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1)); + Assert.Throws(() => gavin.Companies.RemoveAt(20)); + + // Check existance of added companies + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task ListInsertDuplicatedAsync(bool initialize, bool flush, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Insert(4 - i, addedItems[i]); + } + + Assert.That(gavin.Companies[0].ListIndex, Is.EqualTo(4)); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (flush) + { + await (s.FlushAsync(cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListRemoveAtAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {0, 1, 2, 6, 8, 9}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove transient companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(5); + gavin.Companies.RemoveAt(6); + + Assert.That(gavin.Companies.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove persisted companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(3); + gavin.Companies.RemoveAt(3); + + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try remove invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1)); + Assert.Throws(() => gavin.Companies.RemoveAt(8)); + + // Check existance of companies + Sfi.Statistics.Clear(); + var removedIndexes = new HashSet {3, 4, 5, 7}; + for (var i = 0; i < addedItems.Count; i++) + { + Assert.That( + gavin.Companies.Contains(addedItems[i]), + removedIndexes.Contains(i) ? Is.False : (IResolveConstraint) Is.True, + $"Element at index {i} was {(removedIndexes.Contains(i) ? "not " : "")}removed"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListGetSetAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Compare all items + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i], Is.EqualTo(addedItems[i])); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try get invalid indexes + Assert.Throws(() => + { + var item = gavin.Companies[10]; + }); + Assert.Throws(() => + { + var item = gavin.Companies[-1]; + }); + + // Try set invalid indexes + Assert.Throws(() => gavin.Companies[10] = addedItems[0]); + Assert.Throws(() => gavin.Companies[-1] = addedItems[0]); + + // Swap transient and persisted indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var hiIndex = 9 - i; + var tmp = gavin.Companies[i]; + gavin.Companies[i] = gavin.Companies[hiIndex]; + gavin.Companies[hiIndex] = tmp; + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(10)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(finalIndexOrder[i])); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListFlushAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = Enumerable.Range(0, 13).ToList(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Insert + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 15; i < 20; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(20)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 15; i < 20; i++) + { + Assert.That(gavin.Companies.Remove(addedItems[i]), Is.True); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(10)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + gavin.Companies.RemoveAt(10); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(7)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems[i] = item; + Assert.That(((IList)gavin.Companies).Add(item), Is.EqualTo(i)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last transient company + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + Assert.That(gavin.Companies.Remove(addedItems[14]), Is.EqualTo(true)); + var log = sqlLog.GetWholeLog(); + Assert.That(FindAllOccurrences(log, "DELETE \n FROM"), Is.EqualTo(5)); + Assert.That(FindAllOccurrences(log, "INSERT \n INTO"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(14)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test index getter + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies[0], Is.EqualTo(addedItems[0])); + + Assert.That(gavin.Companies.Count, Is.EqualTo(14)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last transient company + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Remove(addedItems[13]), Is.EqualTo(true)); + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test index setter + Sfi.Statistics.Clear(); + gavin.Companies[0] = addedItems[0]; + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test manual flush after remove + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(12); + using (var sqlLog = new SqlLogSpy()) + { + await (s.FlushAsync(cancellationToken)); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(1)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test manual flush after insert + Sfi.Statistics.Clear(); + gavin.Companies.Add(new Company($"c{12}", 12, gavin)); + using (var sqlLog = new SqlLogSpy()) + { + await (s.FlushAsync(cancellationToken)); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(1)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(i)); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListClearAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + gavin.CreditCards.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + var collection = gavin.CreditCards; + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + collection.Insert(i, item); + } + + Assert.That(collection.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Readd two not loaded and two transient permissions + collection.Add(addedItems[0]); + collection.Add(addedItems[1]); + collection.Add(addedItems[5]); + collection.Add(addedItems[6]); + + Assert.That(collection.Count, Is.EqualTo(4)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True); + Assert.That(collection.Remove(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False); + Assert.That(collection.Remove(addedItems[6]), Is.False); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True); + Assert.That(collection.Count, Is.EqualTo(2)); + } + + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + var collection = gavin.CreditCards; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6)); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListIndexOperationsAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var finalIndexOrder = new List {6, 0, 4}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + gavin.Companies.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Current tracker state: + // Indexes: 0,1,2,3,4 + // Queue: / + // RemoveDbIndexes: / + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + Sfi.Statistics.Clear(); + gavin.Companies.Insert(1, new Company("c5", 5, gavin)); + // Current tracker state: + // Indexes: 0,5,1,2,3,4 + // Queue: {1, 5} + // RemoveDbIndexes: / + + gavin.Companies.Insert(0, new Company("c6", 6, gavin)); + // Current tracker state: + // Indexes: 6,0,5,1,2,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: / + + gavin.Companies.RemoveAt(4); + // Current tracker state: + // Indexes: 6,0,5,1,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2,3 + + gavin.Companies.RemoveAt(2); + // Current tracker state: + // Indexes: 6,0,4 + // Queue: {0, 6} + // RemoveDbIndexes: 1,2,3 + + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].OriginalIndex, Is.EqualTo(finalIndexOrder[i])); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetAddAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + Document hia; + Document hia2; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + hia = new Document("HiA", "blah blah blah", gavin); + hia2 = new Document("HiA2", "blah blah blah blah", gavin); + gavin.Documents.Add(hia); + gavin.Documents.Add(hia2); + await (s.PersistAsync(gavin, cancellationToken)); + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + Assert.That(gavin.Documents.Add(document), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(7)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding documents with ICollection interface + Sfi.Statistics.Clear(); + var documents = (ICollection) gavin.Documents; + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + documents.Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + // In this case we cannot determine whether the entities are transient or not so + // we are forced to check the database + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test readding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test readding documents with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + documents.Add(addedDocuments[i]); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of added documents + Sfi.Statistics.Clear(); + foreach (var document in addedDocuments) + { + Assert.That(gavin.Documents.Contains(document), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of not loaded documents + Assert.That(gavin.Documents.Contains(hia), Is.True); + Assert.That(gavin.Documents.Contains(hia2), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of not existing documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Contains(new Document("test1", "content", gavin)), Is.False); + Assert.That(gavin.Documents.Contains(new Document("test2", "content", gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding not loaded documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Add(hia), Is.False); + documents.Add(hia); + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(gavin.Documents.Contains(hia2), Is.True); + Assert.That(gavin.Documents.Contains(hia), Is.True); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task SetAddDuplicatedAsync(bool initialize, bool flush, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new Document($"d{i}", $"c{i}", gavin); + addedItems.Add(item); + gavin.Documents.Add(item); + gavin.Documents.Add(item); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Title, cancellationToken)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedItems[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (flush) + { + await (s.FlushAsync(cancellationToken)); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + } + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetAddTransientAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Permissions.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding permissions with ICollection interface + Sfi.Statistics.Clear(); + var items = (ICollection) gavin.Permissions; + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test readding permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding not loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + + // Test adding permissions with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p3{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test readding permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(10)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding not loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + if (initialize) + { + using (var e = gavin.Permissions.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.True); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetRemoveAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + gavin.Documents.Add(document); + } + + await (s.PersistAsync(gavin, cancellationToken)); + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedDocuments[i] = await (s.GetAsync(addedDocuments[i].Title, cancellationToken)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Add new documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + ((ICollection)gavin.Documents).Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing removed existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.True); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing removed newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetClearAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin", cancellationToken)); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id, cancellationToken)); + } + + var collection = gavin.Permissions; + + Sfi.Statistics.Clear(); + Assert.That(collection.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + collection.Add(item); + } + + Assert.That(collection.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Readd two not loaded and two transient permissions + Assert.That(collection.Add(addedItems[0]), Is.True); + Assert.That(collection.Add(addedItems[1]), Is.True); + Assert.That(collection.Add(addedItems[5]), Is.True); + Assert.That(collection.Add(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(4)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True); + Assert.That(collection.Remove(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False); + Assert.That(collection.Remove(addedItems[6]), Is.False); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True); + Assert.That(collection.Count, Is.EqualTo(2)); + } + + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + var collection = gavin.Permissions; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6)); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapAddAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + UserSetting setting; + var addedSettings = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Test adding settings with Add method + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Test adding settings with [] + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s3{i}", $"data{i}", gavin); + addedSettings.Add(setting); + + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of added settings + Sfi.Statistics.Clear(); + foreach (var item in addedSettings.Skip(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True); + Assert.That(gavin.Settings.Contains(new KeyValuePair(item.Name, item)), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of not loaded settings + foreach (var item in addedSettings.Take(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of not existing settings + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.ContainsKey("test"), Is.False); + Assert.That(gavin.Settings.ContainsKey("test2"), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Try to add an existing setting + Assert.Throws(() => gavin.Settings.Add("s0", new UserSetting("s0", "data", gavin))); + Assert.Throws(() => gavin.Settings.Add("s20", new UserSetting("s20", "data", gavin))); + Assert.Throws(() => gavin.Settings.Add("s30", new UserSetting("s30", "data", gavin))); + + // Get values of not loaded keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s0", out setting), Is.True); + Assert.That(setting.Id, Is.EqualTo(addedSettings[0].Id)); + Assert.That(gavin.Settings["s0"].Id, Is.EqualTo(addedSettings[0].Id)); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Get values of newly added keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s20", out setting), Is.True); + Assert.That(setting, Is.EqualTo(addedSettings[5])); + Assert.That(gavin.Settings["s20"], Is.EqualTo(addedSettings[5])); + Assert.That(gavin.Settings.TryGetValue("s30", out setting), Is.True); + Assert.That(setting, Is.EqualTo(addedSettings[10])); + Assert.That(gavin.Settings["s30"], Is.EqualTo(addedSettings[10])); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Try to get a non existing setting + Assert.That(gavin.Settings.TryGetValue("test", out setting), Is.False); + Assert.That(gavin.Settings.TryGetValue("test2", out setting), Is.False); + Assert.Throws(() => + { + setting = gavin.Settings["test"]; + }); + Assert.Throws(() => + { + setting = gavin.Settings["test2"]; + }); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(4)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(gavin.Settings.ContainsKey(addedSettings[0].Name), Is.True); + Assert.That(gavin.Settings.ContainsKey(addedSettings[5].Name), Is.True); + Assert.That(gavin.Settings.ContainsKey(addedSettings[10].Name), Is.True); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapSetAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that does not exist in db and it is not in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is not in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s1"), Is.True); + Sfi.Statistics.Clear(); + gavin.Settings["s1"] = new UserSetting("s1", "s1", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + await (t.CommitAsync(cancellationToken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapRemoveAsync(bool initialize, CancellationToken cancellationToken = default(CancellationToken)) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin, cancellationToken)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is not in the queue and removal queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s0"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(9)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is in the queue + var item = gavin.Settings["s1"]; + Assert.That(gavin.Settings.Remove("s1"), Is.True); + gavin.Settings.Add(item.Name, item); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s1"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that does not exist in db and it is not in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("test"), Is.False); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s20"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(7)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s2"), Is.True); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s2"), Is.False); + + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + } + + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin", cancellationToken)); + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + await (t.CommitAsync(cancellationToken)); } } @@ -395,5 +3263,22 @@ public async Task AddToUninitializedSetWithLaterLazyLoadAsync() await (t.CommitAsync()); } } + + private int FindAllOccurrences(string source, string substring) + { + if (source == null) + { + return 0; + } + int n = 0, count = 0; + while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) + { + n += substring.Length; + ++count; + } + return count; + } + + private bool IsNativeIdentityGenerator => Dialect.NativeIdentifierGeneratorClass == typeof(IdentityGenerator); } } diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2922/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2922/Fixture.cs new file mode 100644 index 00000000000..1f84df22b83 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2922/Fixture.cs @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2922 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override void OnSetUp() + { + using (var session = OpenSession()) + { + var a = new Store {Id = 1, Name = "A"}; + var b = new Store {Id = 2, Name = "B"}; + var jack = new Employee {Id = 3, Name = "Jack", Store = a}; + a.Staff.Add(jack); + + session.Save(a); + session.Save(b); + session.Save(jack); + session.Flush(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + { + session.Delete("from System.Object"); + session.Flush(); + } + } + + [Test] + public async Task ReparentedCase1Async() + { + using (var session = OpenSession()) + { + var a = await (session.GetAsync(1)); // Order of loading affects outcome + var b = await (session.GetAsync(2)); + + // Move employee to different store + var jack = a.Staff.Single(x => x.Name == "Jack"); + + a.Staff.Remove(jack); + jack.Store = b; + b.Staff.Add(jack); + + await (session.FlushAsync()); + session.Clear(); + } + + using (var session = OpenSession()) + { + var a = await (session.GetAsync(1)); + var b = await (session.GetAsync(2)); + + Assert.That(a.Staff.Count, Is.EqualTo(0)); + Assert.That(b.Staff.Count, Is.EqualTo(1)); + } + } + + [Test] + public async Task ReparentedCase2Async() + { + using (var session = OpenSession()) + { + var b = await (session.GetAsync(2)); + var a = await (session.GetAsync(1)); // Order of loading affects outcome + + // Move employee to different store + var jack = a.Staff.Single(x => x.Name == "Jack"); + + a.Staff.Remove(jack); + jack.Store = b; + b.Staff.Add(jack); + + await (session.FlushAsync()); + session.Clear(); + } + + using (var session = OpenSession()) + { + var jack = await (session.GetAsync(3)); + var b = await (session.GetAsync(2)); + var a = await (session.GetAsync(1)); + + Assert.That(jack, Is.Not.Null); + Assert.That(a.Staff.Count, Is.EqualTo(0)); + Assert.That(b.Staff.Count, Is.EqualTo(1)); + } + } + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/Child.cs b/src/NHibernate.Test/CollectionCompositeKey/Child.cs new file mode 100644 index 00000000000..f6e54ba4eb5 --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/Child.cs @@ -0,0 +1,43 @@ + +namespace NHibernate.Test.CollectionCompositeKey +{ + public class Child + { + protected Child() + { + } + + public Child(int number, string name, Parent parent) + { + Id = new ChildCompositeId() + { + Number = number, + ParentNumber = parent.Number, + ParentCode = parent.Code + }; + Name = name; + } + + public virtual ChildCompositeId Id { get; set; } + + public virtual string Name { get; set; } + + public virtual Parent Parent { get; set; } + + public virtual Parent ParentByName { get; set; } + + public virtual string ParentName { get; set; } + + public virtual Parent ParentByReference { get; set; } + + public virtual string ParentReferenceCode { get; set; } + + public virtual int ParentReferenceNumber { get; set; } + + public virtual string ParentCode { get; set; } + + public virtual int ParentNumber { get; set; } + + public virtual ChildComponent Component { get; set; } = new ChildComponent(); + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/ChildComponent.cs b/src/NHibernate.Test/CollectionCompositeKey/ChildComponent.cs new file mode 100644 index 00000000000..7c6573ef2b0 --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/ChildComponent.cs @@ -0,0 +1,12 @@ + +namespace NHibernate.Test.CollectionCompositeKey +{ + public class ChildComponent + { + public virtual Parent Parent { get; set; } + + public virtual string ParentCode { get; set; } + + public virtual int ParentNumber { get; set; } + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/ChildCompositeId.cs b/src/NHibernate.Test/CollectionCompositeKey/ChildCompositeId.cs new file mode 100644 index 00000000000..2d28814096c --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/ChildCompositeId.cs @@ -0,0 +1,27 @@ + +namespace NHibernate.Test.CollectionCompositeKey +{ + public class ChildCompositeId + { + public virtual int Number { get; set; } + + public virtual string ParentCode { get; set; } + + public virtual int ParentNumber { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is ChildCompositeId key)) + { + return false; + } + + return Number.Equals(key.Number) && ParentCode.Equals(key.ParentCode) && ParentNumber.Equals(key.ParentNumber); + } + + public override int GetHashCode() + { + return Number.GetHashCode() ^ ParentCode.GetHashCode() ^ ParentNumber.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/CollectionOwner.hbm.xml b/src/NHibernate.Test/CollectionCompositeKey/CollectionOwner.hbm.xml new file mode 100644 index 00000000000..e9dba311498 --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/CollectionOwner.hbm.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/CollectionCompositeKey/Fixture.cs b/src/NHibernate.Test/CollectionCompositeKey/Fixture.cs new file mode 100644 index 00000000000..c82c4a0bc2d --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/Fixture.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using NHibernate.Event; +using NHibernate.Persister.Collection; +using NUnit.Framework; +using NUnit.Framework.Constraints; + +namespace NHibernate.Test.CollectionCompositeKey +{ + [TestFixture] + public class Fixture : TestCase + { + private readonly Parent _parentId = new Parent("1", 1); + private int _currentChildId = 1; + private int _currentGrandChildId = 1; + + protected override string[] Mappings => new [] { "CollectionCompositeKey.CollectionOwner.hbm.xml" }; + + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string CacheConcurrencyStrategy => null; + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var parent = CreateParent(_parentId.Code, _parentId.Number); + + s.Save(parent); + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from GrandChild"); + session.Delete("from Child"); + session.Delete("from Parent"); + tx.Commit(); + } + base.OnTearDown(); + } + + [TestCase(nameof(Parent.Children), true, true)] + [TestCase(nameof(Parent.ChildrenByForeignKeys), true, false)] + [TestCase(nameof(Parent.ChildrenByComponent), true, true)] + [TestCase(nameof(Parent.ChildrenByComponentForeignKeys), true, false)] + [TestCase(nameof(Parent.ChildrenNoProperties), false, false)] + [TestCase(nameof(Parent.ChildrenByUniqueKey), true, true)] + [TestCase(nameof(Parent.ChildrenByUniqueKeyNoManyToOne), true, false)] + [TestCase(nameof(Parent.ChildrenByCompositeUniqueKey), true, true)] + [TestCase(nameof(Parent.ChildrenByCompositeUniqueKeyNoManyToOne), true, false)] + public void TestGetElementOwnerForChildCollections(string collectionProperty, bool propertyExists, bool hasManyToOne) + { + TestGetElementOwnerForChildCollections(collectionProperty, propertyExists, hasManyToOne, (parent, child) => child.Id); + } + + [TestCase(nameof(Parent.GrandChildren), true, true)] + public void TestGetElementOwnerForGrandChildCollections(string collectionProperty, bool propertyExists, bool hasManyToOne) + { + TestGetElementOwnerForChildCollections(collectionProperty, propertyExists, hasManyToOne, GetGrandChildId); + } + + private static object GetGrandChildId(Parent parent, GrandChild child) + { + if (parent == null) + { + return null; // Not supported + } + + child.GrandParent = parent; + return child; + } + + private void TestGetElementOwnerForChildCollections(string collectionProperty, bool propertyExists, bool hasManyToOne, Func getChildId) + where TChild : class + { + var persister = Sfi.GetEntityPersister(typeof(Parent).FullName); + var collPersister = GetCollectionPersister(collectionProperty); + TChild firstChild = null; + + var propertyIndex = -1; + for (var i = 0; i < persister.PropertyNames.Length; i++) + { + if (persister.PropertyNames[i] == collectionProperty) + { + propertyIndex = i; + break; + } + } + + Assert.That(propertyIndex, Is.Not.EqualTo(-1)); + + // Test when collection is loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = s.Get(_parentId); + Assert.That(parent, Is.Not.Null); + + var collection = (IList) persister.GetPropertyValue(parent, propertyIndex); + foreach (var child in collection) + { + if (firstChild == null) + { + firstChild = child; + } + + Assert.That(collPersister.GetElementOwner(child, s), propertyExists ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + } + + tx.Commit(); + } + + Assert.That(firstChild, Is.Not.Null); + + // Test when collection is not loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = s.Get(_parentId); + var child = s.Get(getChildId(parent, firstChild)); + Assert.That(parent, Is.Not.Null); + Assert.That(child, Is.Not.Null); + + Assert.That(collPersister.GetElementOwner(child, s), propertyExists ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + + tx.Commit(); + } + + // Test when only the child is loaded + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var id = getChildId(null, firstChild); + if (id != null) + { + var child = s.Get(id); + Assert.That(child, Is.Not.Null); + + Assert.That(collPersister.GetElementOwner(child, s), hasManyToOne ? Is.InstanceOf() : (IResolveConstraint) Is.Null); + } + + tx.Commit(); + } + + // Test transient + using (var s = (IEventSource) OpenSession()) + using (var tx = s.BeginTransaction()) + { + var parent = CreateParent("2", 2); + var collection = (IList) persister.GetPropertyValue(parent, propertyIndex); + + foreach (var child in collection) + { + Assert.That(collPersister.GetElementOwner(child, s), hasManyToOne ? Is.EqualTo(parent) : (IResolveConstraint) Is.Null); + } + + tx.Commit(); + } + } + + private AbstractCollectionPersister GetCollectionPersister(string collectionProperty) + { + return (AbstractCollectionPersister) Sfi.GetCollectionPersister($"{typeof(Parent).FullName}.{collectionProperty}"); + } + + private Parent CreateParent(string code, int number) + { + var parent = new Parent(code, number) + { + Name = $"parent{number}", + ReferenceCode = code, + ReferenceNumber = number + }; + + parent.Children.Add(new Child(_currentChildId++, "child", parent) { Parent = parent }); + parent.ChildrenByForeignKeys.Add(new Child(_currentChildId++, "childFk", parent) { ParentNumber = parent.Number, ParentCode = parent.Code }); + parent.ChildrenByComponent.Add(new Child(_currentChildId++, "childCo", parent) { Component = new ChildComponent { Parent = parent } }); + parent.ChildrenByComponentForeignKeys.Add( + new Child(_currentChildId++, "childCoFk", parent) + { + Component = new ChildComponent { ParentNumber = parent.Number, ParentCode = parent.Code } + }); + parent.ChildrenNoProperties.Add(new Child(_currentChildId++, "childNp", parent)); + parent.ChildrenByUniqueKey.Add(new Child(_currentChildId++, "childUk", parent) { ParentByName = parent }); + parent.ChildrenByUniqueKeyNoManyToOne.Add(new Child(_currentChildId++, "childUkFk", parent) { ParentName = parent.Name }); + parent.ChildrenByCompositeUniqueKey.Add(new Child(_currentChildId++, "childCoUk", parent) { ParentByReference = parent }); + parent.ChildrenByCompositeUniqueKeyNoManyToOne.Add( + new Child(_currentChildId++, "childCoUkFk", parent) + { + ParentReferenceCode = parent.ReferenceCode, + ParentReferenceNumber = parent.ReferenceNumber + }); + + parent.GrandChildren.Add(new GrandChild(_currentGrandChildId, "grandChild", parent)); + + return parent; + } + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/GrandChild.cs b/src/NHibernate.Test/CollectionCompositeKey/GrandChild.cs new file mode 100644 index 00000000000..2c362a18b79 --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/GrandChild.cs @@ -0,0 +1,38 @@ + +namespace NHibernate.Test.CollectionCompositeKey +{ + public class GrandChild + { + protected GrandChild() + { + } + + public GrandChild(int number, string name, Parent grandParent) + { + Number = number; + GrandParent = grandParent; + Name = name; + } + + public virtual int Number { get; set; } + + public virtual Parent GrandParent { get; set; } + + public virtual string Name { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is GrandChild key)) + { + return false; + } + + return Number.Equals(key.Number) && GrandParent.Equals(key.GrandParent); + } + + public override int GetHashCode() + { + return Number.GetHashCode() ^ GrandParent.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/CollectionCompositeKey/Parent.cs b/src/NHibernate.Test/CollectionCompositeKey/Parent.cs new file mode 100644 index 00000000000..b793bb5fe6d --- /dev/null +++ b/src/NHibernate.Test/CollectionCompositeKey/Parent.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.CollectionCompositeKey +{ + public class Parent + { + protected Parent() { } + + public Parent(string code, int number) + { + Code = code; + Number = number; + } + + public virtual string Code { get; set; } + + public virtual int Number { get; set; } + + public virtual string Name { get; set; } + + public virtual int ReferenceNumber { get; set; } + + public virtual string ReferenceCode { get; set; } + + public virtual IList ChildrenByCompositeId { get; set; } = new List(); + + public virtual IList Children { get; set; } = new List(); + + public virtual IList ChildrenByForeignKeys { get; set; } = new List(); + + public virtual IList ChildrenByComponent { get; set; } = new List(); + + public virtual IList ChildrenByComponentForeignKeys { get; set; } = new List(); + + public virtual IList ChildrenByUniqueKey { get; set; } = new List(); + + public virtual IList ChildrenByUniqueKeyNoManyToOne { get; set; } = new List(); + + public virtual IList ChildrenByCompositeUniqueKey { get; set; } = new List(); + + public virtual IList ChildrenByCompositeUniqueKeyNoManyToOne { get; set; } = new List(); + + public virtual IList ChildrenNoProperties { get; set; } = new List(); + + public virtual IList GrandChildren { get; set; } = new List(); + + public override bool Equals(object obj) + { + if (!(obj is Parent key)) + { + return false; + } + + if (string.IsNullOrEmpty(Code)) + { + return ReferenceNumber.Equals(key.ReferenceNumber) && ReferenceCode.Equals(key.ReferenceCode); + } + + return Number.Equals(key.Number) && Code.Equals(key.Code); + } + + public override int GetHashCode() + { + if (string.IsNullOrEmpty(Code)) + { + return ReferenceNumber.GetHashCode() ^ ReferenceCode.GetHashCode(); + } + + return Number.GetHashCode() ^ Code.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/Extralazy/Company.cs b/src/NHibernate.Test/Extralazy/Company.cs new file mode 100644 index 00000000000..728a5f85e8d --- /dev/null +++ b/src/NHibernate.Test/Extralazy/Company.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class Company + { + protected Company() { } + + public Company(string name, int index, User owner) + { + Name = name; + Owner = owner; + OriginalIndex = ListIndex = index; + } + + public virtual int Id { get; set; } + + public virtual int ListIndex { get; set; } + + public virtual int OriginalIndex { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/CreditCard.cs b/src/NHibernate.Test/Extralazy/CreditCard.cs new file mode 100644 index 00000000000..ffeb98a831b --- /dev/null +++ b/src/NHibernate.Test/Extralazy/CreditCard.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class CreditCard + { + protected CreditCard() { } + + public CreditCard(string name, int index, User owner) + { + Name = name; + Owner = owner; + OriginalIndex = ListIndex = index; + } + + public virtual int Id { get; set; } + + public virtual int ListIndex { get; set; } + + public virtual int OriginalIndex { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs b/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs index 248abdf63e8..4aa25902b46 100644 --- a/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs +++ b/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs @@ -1,7 +1,11 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using NHibernate.Cfg; +using NHibernate.Id; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace NHibernate.Test.Extralazy { @@ -23,12 +27,2875 @@ protected override string CacheConcurrencyStrategy get { return null; } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Cfg.Environment.GenerateStatistics, "true"); + } + protected override void OnTearDown() { using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - s.Delete("from System.Object"); + s.Delete("from System.Object"); + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapAddChildSaveChangeParent(bool initialize) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + s.Persist(gavin); + s.Persist(turin); + + for (var i = 0; i < 5; i++) + { + var item = new UserSetting($"g{i}", $"data{i}", gavin); + gavinItems.Add(item); + gavin.Settings.Add(item.Name, item); + + item = new UserSetting($"t{i}", $"data{i}", turin); + turin.Settings.Add(item.Name, item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new UserSetting($"c{i}", $"data{i}", gavin); + s.Save(item); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + gavin.Settings.Add(gavinItems[i].Name, gavinItems[i]); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new UserSetting($"c{i}", $"data{i}", gavin); + gavin.Settings.Add(item.Name, item); + gavinItems.Add(item); + s.Save(item); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Settings.Remove(item.Name); + + item.Owner = turin; + turin.Settings.Add(item.Name, item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Settings.Remove(item.Name); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + s.Delete(item); + } + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(turin.Settings.Count, Is.EqualTo(10)); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListInsertChildSaveChangeParent(bool initialize) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + s.Persist(gavin); + s.Persist(turin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + + item = new Company($"t{i}", i, turin); + turin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + s.Save(item); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + if (i % 2 != 0) + { + gavin.Companies.Insert(i, gavinItems[i]); + } + else + { + gavin.Companies.Add(gavinItems[i]); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + if (i % 2 != 0) + { + gavin.Companies.Insert(i, item); + } + else + { + gavin.Companies.Add(item); + } + + gavinItems.Add(item); + s.Save(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Companies.RemoveAt(5); + + item.Owner = turin; + turin.Companies.Insert(item.ListIndex, item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Companies.RemoveAt(5); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + s.Delete(item); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListClearChildSaveChangeParent(bool initialize) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + s.Persist(gavin); + s.Persist(turin); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"g{i}", i, gavin); + gavin.CreditCards.Add(item); + + item = new CreditCard($"t{i}", i, turin); + turin.CreditCards.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + Sfi.Statistics.Clear(); + gavin.CreditCards.Clear(); + turin.CreditCards.Clear(); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(0)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + // Save credit cards and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c2{i}", i, gavin); + s.Save(item); + gavinItems.Add(item); + } + + for (var i = 0; i < 5; i++) + { + gavin.CreditCards.Add(gavinItems[i]); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + + // Add credit cards to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c2{i}", i, gavin); + Assert.That(((IList) gavin.CreditCards).Add(item), Is.EqualTo(i)); + gavinItems.Add(item); + s.Save(item); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Take(5)) + { + gavin.CreditCards.Remove(item); + + item.Owner = turin; + item.ListIndex += 5; + turin.CreditCards.Add(item); + } + + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.False); + + if (initialize) + { + using (var e = gavin.CreditCards.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.CreditCards.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.CreditCards), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.CreditCards), Is.True); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(5)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(5)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + Assert.That(gavin.CreditCards.Count, Is.EqualTo(10)); + Assert.That(turin.CreditCards.Count, Is.EqualTo(10)); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListAddChildSaveChangeParent(bool initialize) + { + User gavin; + User turin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + turin = new User("turin", "tiger"); + s.Persist(gavin); + s.Persist(turin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + + item = new Company($"t{i}", i, turin); + turin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + s.Save(item); + gavinItems.Add(item); + } + + for (var i = 5; i < 10; i++) + { + gavin.Companies.Add(gavinItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList) gavin.Companies).Add(item), Is.EqualTo(i)); + gavinItems.Add(item); + s.Save(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove added items from the collection and add them to a different parent + foreach (var item in gavinItems.Skip(5).Take(5)) + { + gavin.Companies.Remove(item); + + item.Owner = turin; + turin.Companies.Add(item); + } + + // Remove added items from the collection + for (var i = 10; i < 15; i++) + { + var item = gavinItems[i]; + gavin.Companies.Remove(item); + // When identity is used for the primary key the item will be already inserted in the database, + // so the RemoveAt method will mark it as an orphan which will be deleted on flush. + // The same would work for an initialized collection as the collection snapshot would contain the item. + // When dealing with an id generator that supports a delayed insert, we have to trigger a delete + // for the item as it is currently scheduled for insertion. + if (IsNativeIdentityGenerator) + { + if (i % 2 != 0) + { + item.Owner = null; + } + } + else + { + s.Delete(item); + } + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + using (var e = turin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(NHibernateUtil.IsInitialized(turin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + turin = s.Get("turin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(turin.Companies.Count, Is.EqualTo(10)); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListAddChildSave(bool initialize) + { + User gavin; + var gavinItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"g{i}", i, gavin); + gavinItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Save companies and then add them to the collection + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + s.Save(item); + gavinItems.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(IsNativeIdentityGenerator ? 10 : 5)); + + for (var i = 5; i < 10; i++) + { + gavin.Companies.Add(gavinItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add companies to the collection and then save them + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList) gavinItems).Add(item), Is.EqualTo(i)); + gavin.Companies.Add(item); + s.Save(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListAdd(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test adding companies with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test adding companies with IList interface + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + Assert.That(((IList)addedItems).Add(item), Is.EqualTo(i)); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + + // Check existance of added companies + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + t.Commit(); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void ListAddDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Add(addedItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListInsert(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test inserting companies at the start + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(0, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test inserting companies at the end + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try insert invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1)); + Assert.Throws(() => gavin.Companies.RemoveAt(20)); + + // Check existance of added companies + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + t.Commit(); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void ListInsertDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Insert(4 - i, addedItems[i]); + } + + Assert.That(gavin.Companies[0].ListIndex, Is.EqualTo(4)); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListRemoveAt(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {0, 1, 2, 6, 8, 9}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove transient companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(5); + gavin.Companies.RemoveAt(6); + + Assert.That(gavin.Companies.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove persisted companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(3); + gavin.Companies.RemoveAt(3); + + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try remove invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1)); + Assert.Throws(() => gavin.Companies.RemoveAt(8)); + + // Check existance of companies + Sfi.Statistics.Clear(); + var removedIndexes = new HashSet {3, 4, 5, 7}; + for (var i = 0; i < addedItems.Count; i++) + { + Assert.That( + gavin.Companies.Contains(addedItems[i]), + removedIndexes.Contains(i) ? Is.False : (IResolveConstraint) Is.True, + $"Element at index {i} was {(removedIndexes.Contains(i) ? "not " : "")}removed"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check existance of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(6)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListGetSet(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Compare all items + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i], Is.EqualTo(addedItems[i])); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Try get invalid indexes + Assert.Throws(() => + { + var item = gavin.Companies[10]; + }); + Assert.Throws(() => + { + var item = gavin.Companies[-1]; + }); + + // Try set invalid indexes + Assert.Throws(() => gavin.Companies[10] = addedItems[0]); + Assert.Throws(() => gavin.Companies[-1] = addedItems[0]); + + // Swap transient and persisted indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var hiIndex = 9 - i; + var tmp = gavin.Companies[i]; + gavin.Companies[i] = gavin.Companies[hiIndex]; + gavin.Companies[hiIndex] = tmp; + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(10)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Check indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(finalIndexOrder[i])); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListFlush(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = Enumerable.Range(0, 13).ToList(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Insert + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 15; i < 20; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(20)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 15; i < 20; i++) + { + Assert.That(gavin.Companies.Remove(addedItems[i]), Is.True); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(10)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + gavin.Companies.RemoveAt(10); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(7)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems[i] = item; + Assert.That(((IList)gavin.Companies).Add(item), Is.EqualTo(i)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last transient company + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + Assert.That(gavin.Companies.Remove(addedItems[14]), Is.EqualTo(true)); + var log = sqlLog.GetWholeLog(); + Assert.That(FindAllOccurrences(log, "DELETE \n FROM"), Is.EqualTo(5)); + Assert.That(FindAllOccurrences(log, "INSERT \n INTO"), Is.EqualTo(5)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(14)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test index getter + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies[0], Is.EqualTo(addedItems[0])); + + Assert.That(gavin.Companies.Count, Is.EqualTo(14)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Remove last transient company + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Remove(addedItems[13]), Is.EqualTo(true)); + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test index setter + Sfi.Statistics.Clear(); + gavin.Companies[0] = addedItems[0]; + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test manual flush after remove + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(12); + using (var sqlLog = new SqlLogSpy()) + { + s.Flush(); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(1)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + // Test manual flush after insert + Sfi.Statistics.Clear(); + gavin.Companies.Add(new Company($"c{12}", 12, gavin)); + using (var sqlLog = new SqlLogSpy()) + { + s.Flush(); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(1)); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(i)); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(13)); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListClear(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + gavin.CreditCards.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + var collection = gavin.CreditCards; + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + collection.Insert(i, item); + } + + Assert.That(collection.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Readd two not loaded and two transient permissions + collection.Add(addedItems[0]); + collection.Add(addedItems[1]); + collection.Add(addedItems[5]); + collection.Add(addedItems[6]); + + Assert.That(collection.Count, Is.EqualTo(4)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True); + Assert.That(collection.Remove(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False); + Assert.That(collection.Remove(addedItems[6]), Is.False); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True); + Assert.That(collection.Count, Is.EqualTo(2)); + } + + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + var collection = gavin.CreditCards; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6)); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListIndexOperations(bool initialize) + { + User gavin; + var finalIndexOrder = new List {6, 0, 4}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Current tracker state: + // Indexes: 0,1,2,3,4 + // Queue: / + // RemoveDbIndexes: / + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + Sfi.Statistics.Clear(); + gavin.Companies.Insert(1, new Company("c5", 5, gavin)); + // Current tracker state: + // Indexes: 0,5,1,2,3,4 + // Queue: {1, 5} + // RemoveDbIndexes: / + + gavin.Companies.Insert(0, new Company("c6", 6, gavin)); + // Current tracker state: + // Indexes: 6,0,5,1,2,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: / + + gavin.Companies.RemoveAt(4); + // Current tracker state: + // Indexes: 6,0,5,1,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2,3 + + gavin.Companies.RemoveAt(2); + // Current tracker state: + // Indexes: 6,0,4 + // Queue: {0, 6} + // RemoveDbIndexes: 1,2,3 + + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False); + + gavin.UpdateCompaniesIndexes(); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].OriginalIndex, Is.EqualTo(finalIndexOrder[i])); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(3)); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetAdd(bool initialize) + { + User gavin; + Document hia; + Document hia2; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + hia = new Document("HiA", "blah blah blah", gavin); + hia2 = new Document("HiA2", "blah blah blah blah", gavin); + gavin.Documents.Add(hia); + gavin.Documents.Add(hia2); + s.Persist(gavin); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + Assert.That(gavin.Documents.Add(document), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(7)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding documents with ICollection interface + Sfi.Statistics.Clear(); + var documents = (ICollection) gavin.Documents; + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + documents.Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + // In this case we cannot determine whether the entities are transient or not so + // we are forced to check the database + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test readding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test readding documents with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + documents.Add(addedDocuments[i]); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of added documents + Sfi.Statistics.Clear(); + foreach (var document in addedDocuments) + { + Assert.That(gavin.Documents.Contains(document), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of not loaded documents + Assert.That(gavin.Documents.Contains(hia), Is.True); + Assert.That(gavin.Documents.Contains(hia2), Is.True); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Check existance of not existing documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Contains(new Document("test1", "content", gavin)), Is.False); + Assert.That(gavin.Documents.Contains(new Document("test2", "content", gavin)), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test adding not loaded documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Add(hia), Is.False); + documents.Add(hia); + + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(12)); + Assert.That(gavin.Documents.Contains(hia2), Is.True); + Assert.That(gavin.Documents.Contains(hia), Is.True); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + t.Commit(); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void SetAddDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Document($"d{i}", $"c{i}", gavin); + addedItems.Add(item); + gavin.Documents.Add(item); + gavin.Documents.Add(item); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Title); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Readd items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedItems[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + } + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetAddTransient(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Permissions.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding permissions with ICollection interface + Sfi.Statistics.Clear(); + var items = (ICollection) gavin.Permissions; + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test readding permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding not loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + + // Test adding permissions with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p3{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test readding permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(10)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding not loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + // Test adding loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + if (initialize) + { + using (var e = gavin.Permissions.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.True); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetRemove(bool initialize) + { + User gavin; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + gavin.Documents.Add(document); + } + + s.Persist(gavin); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedDocuments[i] = s.Get(addedDocuments[i].Title); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Add new documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + ((ICollection)gavin.Documents).Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing removed existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.True); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing removed newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True); + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetClear(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + var collection = gavin.Permissions; + + Sfi.Statistics.Clear(); + Assert.That(collection.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + collection.Add(item); + } + + Assert.That(collection.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Readd two not loaded and two transient permissions + Assert.That(collection.Add(addedItems[0]), Is.True); + Assert.That(collection.Add(addedItems[1]), Is.True); + Assert.That(collection.Add(addedItems[5]), Is.True); + Assert.That(collection.Add(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(4)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True); + Assert.That(collection.Remove(addedItems[6]), Is.True); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False); + Assert.That(collection.Remove(addedItems[6]), Is.False); + + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True); + Assert.That(collection.Count, Is.EqualTo(2)); + } + + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + var collection = gavin.Permissions; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6)); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapAdd(bool initialize) + { + User gavin; + UserSetting setting; + var addedSettings = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Test adding settings with Add method + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Test adding settings with [] + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s3{i}", $"data{i}", gavin); + addedSettings.Add(setting); + + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of added settings + Sfi.Statistics.Clear(); + foreach (var item in addedSettings.Skip(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True); + Assert.That(gavin.Settings.Contains(new KeyValuePair(item.Name, item)), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of not loaded settings + foreach (var item in addedSettings.Take(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Check existance of not existing settings + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.ContainsKey("test"), Is.False); + Assert.That(gavin.Settings.ContainsKey("test2"), Is.False); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Try to add an existing setting + Assert.Throws(() => gavin.Settings.Add("s0", new UserSetting("s0", "data", gavin))); + Assert.Throws(() => gavin.Settings.Add("s20", new UserSetting("s20", "data", gavin))); + Assert.Throws(() => gavin.Settings.Add("s30", new UserSetting("s30", "data", gavin))); + + // Get values of not loaded keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s0", out setting), Is.True); + Assert.That(setting.Id, Is.EqualTo(addedSettings[0].Id)); + Assert.That(gavin.Settings["s0"].Id, Is.EqualTo(addedSettings[0].Id)); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Get values of newly added keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s20", out setting), Is.True); + Assert.That(setting, Is.EqualTo(addedSettings[5])); + Assert.That(gavin.Settings["s20"], Is.EqualTo(addedSettings[5])); + Assert.That(gavin.Settings.TryGetValue("s30", out setting), Is.True); + Assert.That(setting, Is.EqualTo(addedSettings[10])); + Assert.That(gavin.Settings["s30"], Is.EqualTo(addedSettings[10])); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Try to get a non existing setting + Assert.That(gavin.Settings.TryGetValue("test", out setting), Is.False); + Assert.That(gavin.Settings.TryGetValue("test2", out setting), Is.False); + Assert.Throws(() => + { + setting = gavin.Settings["test"]; + }); + Assert.Throws(() => + { + setting = gavin.Settings["test2"]; + }); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(4)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(15)); + Assert.That(gavin.Settings.ContainsKey(addedSettings[0].Name), Is.True); + Assert.That(gavin.Settings.ContainsKey(addedSettings[5].Name), Is.True); + Assert.That(gavin.Settings.ContainsKey(addedSettings[10].Name), Is.True); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapSet(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that does not exist in db and it is not in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is not in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Set a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s1"), Is.True); + Sfi.Statistics.Clear(); + gavin.Settings["s1"] = new UserSetting("s1", "s1", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapRemove(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is not in the queue and removal queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s0"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(9)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is in the queue + var item = gavin.Settings["s1"]; + Assert.That(gavin.Settings.Remove("s1"), Is.True); + gavin.Settings.Add(item.Name, item); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s1"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that does not exist in db and it is not in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("test"), Is.False); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s20"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(7)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + // Remove a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s2"), Is.True); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s2"), Is.False); + + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0)); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True); + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(6)); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False); + t.Commit(); } } @@ -384,5 +3251,22 @@ public void AddToUninitializedSetWithLaterLazyLoad() t.Commit(); } } + + private int FindAllOccurrences(string source, string substring) + { + if (source == null) + { + return 0; + } + int n = 0, count = 0; + while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) + { + n += substring.Length; + ++count; + } + return count; + } + + private bool IsNativeIdentityGenerator => Dialect.NativeIdentifierGeneratorClass == typeof(IdentityGenerator); } } diff --git a/src/NHibernate.Test/Extralazy/User.cs b/src/NHibernate.Test/Extralazy/User.cs index 5279d6d0dd6..0ab0f492b5b 100644 --- a/src/NHibernate.Test/Extralazy/User.cs +++ b/src/NHibernate.Test/Extralazy/User.cs @@ -46,5 +46,29 @@ public virtual ISet Photos get { return photos; } set { photos = value; } } + + public virtual IDictionary Settings { get; set; } = new Dictionary(); + + public virtual ISet Permissions { get; set; } = new HashSet(); + + public virtual IList Companies { get; set; } = new List(); + + public virtual IList CreditCards { get; set; } = new List(); + + public virtual void UpdateCompaniesIndexes() + { + for (var i = 0; i < Companies.Count; i++) + { + Companies[i].ListIndex = i; + } + } + + public virtual void UpdateCreditCardsIndexes() + { + for (var i = 0; i < CreditCards.Count; i++) + { + CreditCards[i].ListIndex = i; + } + } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml b/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml index 19d201f2bd4..9641c5800c6 100644 --- a/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml +++ b/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml @@ -21,6 +21,11 @@ + + + + + @@ -29,11 +34,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Extralazy/UserPermission.cs b/src/NHibernate.Test/Extralazy/UserPermission.cs new file mode 100644 index 00000000000..3c4820c0c72 --- /dev/null +++ b/src/NHibernate.Test/Extralazy/UserPermission.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class UserPermission + { + protected UserPermission() { } + + public UserPermission(string name, User owner) + { + Name = name; + Owner = owner; + } + + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/UserSetting.cs b/src/NHibernate.Test/Extralazy/UserSetting.cs new file mode 100644 index 00000000000..da86331a3f3 --- /dev/null +++ b/src/NHibernate.Test/Extralazy/UserSetting.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class UserSetting + { + protected UserSetting() { } + public UserSetting(string name, string data, User owner) + { + Name = name; + Data = data; + Owner = owner; + } + + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual string Data { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2922/Domain.cs b/src/NHibernate.Test/NHSpecificTest/NH2922/Domain.cs new file mode 100644 index 00000000000..2ef8c941cc6 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2922/Domain.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH2922 +{ + public class Employee + { + public int Id { get; set; } + public string Name { get; set; } + public Store Store { get; set; } + } + + public class Store + { + public int Id { get; set; } + public string Name { get; set; } + public IList Staff { get; set; } + + public Store() + { + Staff = new List(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2922/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2922/Fixture.cs new file mode 100644 index 00000000000..1578991ef7e --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2922/Fixture.cs @@ -0,0 +1,94 @@ +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2922 +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override void OnSetUp() + { + using (var session = OpenSession()) + { + var a = new Store {Id = 1, Name = "A"}; + var b = new Store {Id = 2, Name = "B"}; + var jack = new Employee {Id = 3, Name = "Jack", Store = a}; + a.Staff.Add(jack); + + session.Save(a); + session.Save(b); + session.Save(jack); + session.Flush(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + { + session.Delete("from System.Object"); + session.Flush(); + } + } + + [Test] + public void ReparentedCase1() + { + using (var session = OpenSession()) + { + var a = session.Get(1); // Order of loading affects outcome + var b = session.Get(2); + + // Move employee to different store + var jack = a.Staff.Single(x => x.Name == "Jack"); + + a.Staff.Remove(jack); + jack.Store = b; + b.Staff.Add(jack); + + session.Flush(); + session.Clear(); + } + + using (var session = OpenSession()) + { + var a = session.Get(1); + var b = session.Get(2); + + Assert.That(a.Staff.Count, Is.EqualTo(0)); + Assert.That(b.Staff.Count, Is.EqualTo(1)); + } + } + + [Test] + public void ReparentedCase2() + { + using (var session = OpenSession()) + { + var b = session.Get(2); + var a = session.Get(1); // Order of loading affects outcome + + // Move employee to different store + var jack = a.Staff.Single(x => x.Name == "Jack"); + + a.Staff.Remove(jack); + jack.Store = b; + b.Staff.Add(jack); + + session.Flush(); + session.Clear(); + } + + using (var session = OpenSession()) + { + var jack = session.Get(3); + var b = session.Get(2); + var a = session.Get(1); + + Assert.That(jack, Is.Not.Null); + Assert.That(a.Staff.Count, Is.EqualTo(0)); + Assert.That(b.Staff.Count, Is.EqualTo(1)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2922/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH2922/Mappings.hbm.xml new file mode 100644 index 00000000000..4313865a2ec --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2922/Mappings.hbm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs b/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs index 00b1c3cccb3..6c115dff5b4 100644 --- a/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs +++ b/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs @@ -12,11 +12,14 @@ using System.Collections; using System.Collections.Generic; using System.Data.Common; +using System.Linq; using NHibernate.Collection.Generic; +using NHibernate.Collection.Trackers; using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader; using NHibernate.Persister.Collection; +using NHibernate.Proxy; using NHibernate.Type; using NHibernate.Util; @@ -27,6 +30,62 @@ namespace NHibernate.Collection public abstract partial class AbstractPersistentCollection : IPersistentCollection, ILazyInitializedCollection { + protected virtual async Task ReadKeyExistenceAsync(TKey elementKey, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + await (session.FlushAsync(cancellationToken)).ConfigureAwait(false); + } + + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + + if (queueOperationTracker.ContainsKey(elementKey)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + return false; + } + + // As keys are unordered we don't have to calculate the current order of the key + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + Read(); + } + return null; + } + + internal async Task IsTransientAsync(object element, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var queryableCollection = (IQueryableCollection) Session.Factory.GetCollectionPersister(Role); + return + queryableCollection != null && + queryableCollection.ElementType.IsEntityType && + !element.IsProxy() && + !Session.PersistenceContext.IsEntryFor(element) && + await (ForeignKeys.IsTransientFastAsync(queryableCollection.ElementPersister.EntityName, element, Session, cancellationToken)).ConfigureAwait(false) == true; + } + /// /// Initialize the collection, if possible, wrapping any exceptions /// in a runtime exception @@ -102,20 +161,35 @@ public Task GetQueuedOrphansAsync(string entityName, CancellationTo { if (HasQueuedOperations) { - List additions = new List(operationQueue.Count); - List removals = new List(operationQueue.Count); - for (int i = 0; i < operationQueue.Count; i++) + List additions; + List removals; + + // Use the queue operation tracker when available to get the orphans as the default logic + // does not work correctly when readding a transient entity. Removals list should + // contain only entities that are already in the database. + if (_queueOperationTracker != null) { - IDelayedOperation op = operationQueue[i]; - if (op.AddedInstance != null) - { - additions.Add(op.AddedInstance); - } - if (op.Orphan != null) + removals = new List(_queueOperationTracker.GetOrphans().Cast()); + additions = new List(_queueOperationTracker.GetAddedElements().Cast()); + } + else // 6.0 TODO: Remove whole else block + { + additions = new List(operationQueue.Count); + removals = new List(operationQueue.Count); + for (int i = 0; i < operationQueue.Count; i++) { - removals.Add(op.Orphan); + var op = operationQueue[i]; + if (op.AddedInstance != null) + { + additions.Add(op.AddedInstance); + } + if (op.Orphan != null) + { + removals.Add(op.Orphan); + } } } + return GetOrphansAsync(removals, additions, entityName, session, cancellationToken); } @@ -162,12 +236,7 @@ public virtual Task PreInsertAsync(ICollectionPersister persister, CancellationT protected virtual async Task GetOrphansAsync(ICollection oldElements, ICollection currentElements, string entityName, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - // short-circuit(s) - if (currentElements.Count == 0) - { - // no new elements, the old list contains only Orphans - return oldElements; - } + var collectionPersister = session.PersistenceContext.GetCollectionEntry(this).LoadedPersister as AbstractCollectionPersister; if (oldElements.Count == 0) { // no old elements, so no Orphans neither @@ -193,6 +262,11 @@ protected virtual async Task GetOrphansAsync(ICollection oldElement // iterate over the *old* list foreach (object old in oldElements) { + if (!IsOrphan(old, collectionPersister)) + { + continue; + } + object oldId = await (ForeignKeys.GetEntityIdentifierIfNotUnsavedAsync(entityName, old, session, cancellationToken)).ConfigureAwait(false); if (!currentIds.Contains(new TypedValue(idType, oldId, false))) { diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs index fc1d89befe1..6ec7054d7d2 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs index 260877f54b3..280a01c2882 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs index 32819bb279b..36de58b00d4 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs index f20da3da39e..6e519eb6b18 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -169,4 +170,4 @@ public override Task NeedsUpdatingAsync(object entry, int i, IType elemTyp } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/Event/Default/AbstractFlushingEventListener.cs b/src/NHibernate/Async/Event/Default/AbstractFlushingEventListener.cs index dcea3f46739..e375ee88ac2 100644 --- a/src/NHibernate/Async/Event/Default/AbstractFlushingEventListener.cs +++ b/src/NHibernate/Async/Event/Default/AbstractFlushingEventListener.cs @@ -45,18 +45,18 @@ protected virtual async Task FlushEverythingToExecutionsAsync(FlushEvent @event, session.Interceptor.PreFlush((ICollection) persistenceContext.EntitiesByKey.Values); - await (PrepareEntityFlushesAsync(session, cancellationToken)).ConfigureAwait(false); - // we could move this inside if we wanted to - // tolerate collection initializations during - // collection dirty checking: - await (PrepareCollectionFlushesAsync(session, cancellationToken)).ConfigureAwait(false); - // now, any collections that are initialized - // inside this block do not get updated - they - // are ignored until the next flush - persistenceContext.Flushing = true; try { + await (PrepareEntityFlushesAsync(session, cancellationToken)).ConfigureAwait(false); + // we could move this inside if we wanted to + // tolerate collection initializations during + // collection dirty checking: + await (PrepareCollectionFlushesAsync(session, cancellationToken)).ConfigureAwait(false); + // now, any collections that are initialized + // inside this block do not get updated - they + // are ignored until the next flush + await (FlushEntitiesAsync(@event, cancellationToken)).ConfigureAwait(false); await (FlushCollectionsAsync(session, cancellationToken)).ConfigureAwait(false); } diff --git a/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs b/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs index 985ead87deb..8ce0c01ff95 100644 --- a/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs +++ b/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs @@ -10,13 +10,14 @@ using System; using System.Collections; - using NHibernate.Action; using NHibernate.Classic; +using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Id; using NHibernate.Impl; using NHibernate.Intercept; +using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; using NHibernate.Type; using Status=NHibernate.Engine.Status; @@ -235,6 +236,13 @@ protected virtual async Task PerformSaveOrReplicateAsync(object entity, key = source.GenerateEntityKey(id, persister); source.PersistenceContext.CheckUniqueness(key, entity); //source.getBatcher().executeBatch(); //found another way to ensure that all batched joined inserts have been executed + + // Update uninitialized collections that contain the inserted child (NH-739). We don't need to update the collections + // when doing a full flush as they will execute all queued actions at once. + if (!source.PersistenceContext.Flushing) + { + await (UpdateCollectionsQueuesAsync(source, persister, entity, cancellationToken)).ConfigureAwait(false); + } } else { @@ -268,6 +276,55 @@ protected virtual async Task PerformSaveOrReplicateAsync(object entity, return id; } + private static async Task UpdateCollectionsQueuesAsync(ISessionImplementor source, IEntityPersister persister, object entity, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var roles = source.Factory.GetCollectionRolesByEntityParticipant(persister.EntityName); + if (roles == null) + { + return; + } + + foreach (var role in roles) + { + if (!(source.Factory.GetCollectionPersister(role) is AbstractCollectionPersister collectionPersister)) + { + continue; + } + + var ownerKey = await (collectionPersister.GetElementOwnerKeyAsync(entity, source, cancellationToken)).ConfigureAwait(false); + if (ownerKey == null) + { + continue; + } + + var colKey = new CollectionKey(collectionPersister, ownerKey); + var collection = source.PersistenceContext.GetCollection(colKey); + if (collection == null || + collection.WasInitialized || + !(collection is AbstractPersistentCollection persistentCollection)) + { + continue; + } + + var queueTracker = persistentCollection.GetOrCreateQueueOperationTracker(); + if (queueTracker == null) + { + continue; + } + + // We have to reset the cached sizes in order to avoid having an incorrect value + // for ICollection.Count + persistentCollection.ResetCachedSize(); + queueTracker.DatabaseCollectionSize = -1; + queueTracker.AfterElementFlushing(entity); + if (persistentCollection.IsDirty && !persistentCollection.HasQueuedOperations) + { + persistentCollection.ClearDirty(); + } + } + } + protected virtual async Task VisitCollectionsBeforeSaveAsync(object entity, object id, object[] values, IType[] types, IEventSource source, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Persister/Collection/AbstractCollectionPersister.cs b/src/NHibernate/Async/Persister/Collection/AbstractCollectionPersister.cs index eb15d667603..90ae2f0fb3b 100644 --- a/src/NHibernate/Async/Persister/Collection/AbstractCollectionPersister.cs +++ b/src/NHibernate/Async/Persister/Collection/AbstractCollectionPersister.cs @@ -199,6 +199,23 @@ protected async Task WriteIdentifierAsync(DbCommand st, object idx, int i, return i + 1; } + internal Task GetElementOwnerKeyAsync(object element, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + var owner = GetElementOwner(element, session); + return owner == null ? Task.FromResult(null ): CollectionType.GetKeyOfOwnerAsync(owner, session, cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + public async Task RemoveAsync(object id, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Collection/AbstractPersistentCollection.cs b/src/NHibernate/Collection/AbstractPersistentCollection.cs index 9387b9f8ff0..f7c59affa87 100644 --- a/src/NHibernate/Collection/AbstractPersistentCollection.cs +++ b/src/NHibernate/Collection/AbstractPersistentCollection.cs @@ -2,11 +2,14 @@ using System.Collections; using System.Collections.Generic; using System.Data.Common; +using System.Linq; using NHibernate.Collection.Generic; +using NHibernate.Collection.Trackers; using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader; using NHibernate.Persister.Collection; +using NHibernate.Proxy; using NHibernate.Type; using NHibernate.Util; @@ -19,9 +22,15 @@ namespace NHibernate.Collection [Serializable] public abstract partial class AbstractPersistentCollection : IPersistentCollection, ILazyInitializedCollection { + // Since v5.3 + [Obsolete("This field has no more usages in NHibernate and will be removed in a future version.")] protected internal static readonly object Unknown = new object(); //place holder + // Since v5.3 + [Obsolete("This field has no more usages in NHibernate and will be removed in a future version.")] protected internal static readonly object NotFound = new object(); //place holder + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected interface IDelayedOperation { object AddedInstance { get; } @@ -29,6 +38,7 @@ protected interface IDelayedOperation void Operate(); } + // 6.0 TODO: Remove private class AdditionEnumerable : IEnumerable { private readonly AbstractPersistentCollection enclosingInstance; @@ -85,9 +95,12 @@ public void Reset() [NonSerialized] private ISessionImplementor session; private bool initialized; - [NonSerialized] private List operationQueue; +#pragma warning disable 618 + [NonSerialized] private List operationQueue; // 6.0 TODO: Remove +#pragma warning restore 618 [NonSerialized] private bool directlyAccessible; [NonSerialized] private bool initializing; + [NonSerialized] private AbstractQueueOperationTracker _queueOperationTracker; private object owner; private int cachedSize = -1; @@ -213,6 +226,11 @@ protected bool InverseOneToManyOrNoOrphanDelete } } + internal void ResetCachedSize() + { + cachedSize = -1; + } + /// /// Return the user-visible collection (or array) instance /// @@ -276,26 +294,41 @@ protected virtual bool ReadSize() { return true; } - else + + ThrowLazyInitializationExceptionIfNotConnected(); + var entry = session.PersistenceContext.GetCollectionEntry(this); + var persister = entry.LoadedPersister; + if (persister.IsExtraLazy) { - ThrowLazyInitializationExceptionIfNotConnected(); - CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); - ICollectionPersister persister = entry.LoadedPersister; - if (persister.IsExtraLazy) + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) { if (HasQueuedOperations) { session.Flush(); } + cachedSize = persister.GetSize(entry.LoadedKey, session); return true; } + + if (!queueOperationTracker.Cleared && queueOperationTracker.DatabaseCollectionSize < 0) + { + queueOperationTracker.DatabaseCollectionSize = persister.GetSize(entry.LoadedKey, session); + } + + cachedSize = queueOperationTracker.Cleared + ? queueOperationTracker.GetQueueSize() + : queueOperationTracker.GetCollectionSize(); + return true; } + Read(); } - Read(); return false; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual bool? ReadIndexExistence(object index) { if (!initialized) @@ -316,6 +349,8 @@ protected virtual bool ReadSize() return null; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual bool? ReadElementExistence(object element) { if (!initialized) @@ -336,6 +371,8 @@ protected virtual bool ReadSize() return null; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual object ReadElementByIndex(object index) { if (!initialized) @@ -357,6 +394,232 @@ protected virtual object ReadElementByIndex(object index) return Unknown; } + protected virtual bool? ReadKeyExistence(TKey elementKey) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + + if (queueOperationTracker.ContainsKey(elementKey)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + return false; + } + + // As keys are unordered we don't have to calculate the current order of the key + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + Read(); + } + return null; + } + + protected virtual bool? ReadElementExistence(T element, out bool? existsInDb) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractCollectionQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + + existsInDb = persister.ElementExists(entry.LoadedKey, element, session); + return existsInDb; + } + + if (queueOperationTracker.ContainsElement(element)) + { + existsInDb = null; + return true; + } + + if (queueOperationTracker.Cleared) + { + existsInDb = null; + return false; + } + + if (queueOperationTracker.IsElementQueuedForDelete(element)) + { + existsInDb = null; + return false; + } + + existsInDb = persister.ElementExists(entry.LoadedKey, element, session); + return existsInDb; + } + Read(); + } + + existsInDb = null; + return null; + } + + protected virtual bool? TryReadElementByKey(TKey elementKey, out TValue element, out bool? existsInDb) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + } + else + { + if (queueOperationTracker.TryGetElementByKey(elementKey, out element)) + { + existsInDb = null; + return true; + } + + if (queueOperationTracker.Cleared) + { + existsInDb = null; + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + existsInDb = null; + return false; + } + + if (queueOperationTracker.TryGetDatabaseElementByKey(elementKey, out element)) + { + existsInDb = true; + return true; + } + } + + var elementByKey = persister.GetElementByIndex(entry.LoadedKey, elementKey, session, owner); + if (persister.NotFoundObject == elementByKey) + { + element = default(TValue); + existsInDb = false; + return false; + } + + element = (TValue) elementByKey; + existsInDb = true; + return true; + } + Read(); + } + + element = default(TValue); + existsInDb = null; + return null; + } + + protected virtual bool? TryReadElementAtIndex(int index, out T element) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractCollectionQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + } + else if (HasQueuedOperations) + { + if (queueOperationTracker.RequiresFlushing(nameof(queueOperationTracker.TryGetElementAtIndex))) + { + session.Flush(); + } + else + { + if (queueOperationTracker.DatabaseCollectionSize < 0 && + queueOperationTracker.RequiresDatabaseCollectionSize(nameof(queueOperationTracker.TryGetElementAtIndex)) && + !ReadSize()) + { + element = default(T); + return null; // The collection was loaded + } + + if (queueOperationTracker.TryGetElementAtIndex(index, out element)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + // We have to calculate the database index based on the changes in the queue + index = queueOperationTracker.CalculateDatabaseElementIndex(index); + if (index < 0) + { + element = default(T); + return false; + } + } + } + + var elementByIndex = persister.GetElementByIndex(entry.LoadedKey, index, session, owner); + if (persister.NotFoundObject == elementByIndex) + { + element = default(T); + return false; + } + + element = (T) elementByIndex; + return true; + } + Read(); + } + + element = default(T); + return null; + } + /// /// Called by any writer method of the collection interface /// @@ -366,9 +629,35 @@ protected virtual void Write() Dirty(); } + internal virtual AbstractQueueOperationTracker CreateQueueOperationTracker() => null; + + internal AbstractQueueOperationTracker QueueOperationTracker + { + get => _queueOperationTracker; + set => _queueOperationTracker = value; + } + + internal AbstractQueueOperationTracker GetOrCreateQueueOperationTracker() + { + if (!IsOperationQueueEnabled) + { + return null; + } + + if (_queueOperationTracker != null) + { + return _queueOperationTracker; + } + + _queueOperationTracker = CreateQueueOperationTracker(); + return _queueOperationTracker; + } + /// /// Queue an addition, delete etc. if the persistent collection supports it /// + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual void QueueOperation(IDelayedOperation element) { if (operationQueue == null) @@ -379,6 +668,136 @@ protected virtual void QueueOperation(IDelayedOperation element) dirty = true; //needed so that we remove this collection from the second-level cache } + protected bool QueueAddElement(T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.AddElement)); + var result = queueOperationTracker.AddElement(element); + UpdateCachedFields(queueOperationTracker); + return result; + } + + protected void QueueRemoveExistingElement(T element, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.RemoveExistingElement), out var wasFlushed); + queueOperationTracker.RemoveExistingElement(element, wasFlushed ? true : existsInDb); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueRemoveElementAtIndex(int index, T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.RemoveElementAtIndex)); + queueOperationTracker.RemoveElementAtIndex(index, element); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueAddElementAtIndex(int index, T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.AddElementAtIndex)); + queueOperationTracker.AddElementAtIndex(index, element); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueSetElementAtIndex(int index, T element, T oldElement) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.SetElementAtIndex)); + queueOperationTracker.SetElementAtIndex(index, element, oldElement); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueClearCollection() + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractQueueOperationTracker.ClearCollection), out _); + queueOperationTracker.ClearCollection(); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueAddElementByKey(TKey elementKey, TValue element, bool exists) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.AddElementByKey)); + queueOperationTracker.AddElementByKey(elementKey, element, exists); + UpdateCachedFields(queueOperationTracker); + } + + protected void QueueSetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.SetElementByKey)); + queueOperationTracker.SetElementByKey(elementKey, element, oldElement, existsInDb); + UpdateCachedFields(queueOperationTracker); + } + + protected bool QueueRemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.RemoveElementByKey)); + var result = queueOperationTracker.RemoveElementByKey(elementKey, oldElement, existsInDb); + UpdateCachedFields(queueOperationTracker); + return result; + } + + private void UpdateCachedFields(AbstractQueueOperationTracker tracker) + { + dirty = tracker.HasChanges(); + if (cachedSize < 0) + { + return; + } + + cachedSize = tracker.Cleared ? tracker.GetQueueSize() : tracker.GetCollectionSize(); + } + + private AbstractMapQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName) + { + return (AbstractMapQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out _); + } + + private AbstractCollectionQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName) + { + return (AbstractCollectionQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out _); + } + + private AbstractCollectionQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName, out bool wasFlushed) + { + return (AbstractCollectionQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out wasFlushed); + } + + private AbstractQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName, out bool wasFlushed) + { + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + throw new InvalidOperationException($"{nameof(CreateQueueOperationTracker)} must return a not null value."); + } + + if (queueOperationTracker.RequiresFlushing(operationName)) + { + session.Flush(); + wasFlushed = true; + } + else + { + wasFlushed = false; + } + + queueOperationTracker.BeforeOperation(operationName); + if (queueOperationTracker.DatabaseCollectionSize < 0 && queueOperationTracker.RequiresDatabaseCollectionSize(operationName) && !ReadSize()) + { + throw new InvalidOperationException($"The collection role {Role} must be mapped as extra lazy."); + } + + dirty = true; // Needed so that we remove this collection from the second-level cache + return queueOperationTracker; + } + + internal bool IsTransient(object element) + { + var queryableCollection = (IQueryableCollection) Session.Factory.GetCollectionPersister(Role); + return + queryableCollection != null && + queryableCollection.ElementType.IsEntityType && + !element.IsProxy() && + !Session.PersistenceContext.IsEntryFor(element) && + ForeignKeys.IsTransientFast(queryableCollection.ElementPersister.EntityName, element, Session) == true; + } + // Obsolete since v5.2 /// /// After reading all existing elements from the database, @@ -426,6 +845,7 @@ public void SetSnapshot(object key, string role, object snapshot) public virtual void PostAction() { operationQueue = null; + _queueOperationTracker?.AfterFlushing(); cachedSize = -1; ClearDirty(); } @@ -456,7 +876,7 @@ public virtual bool AfterInitialize(ICollectionPersister persister) { SetInitialized(); cachedSize = -1; - return operationQueue == null; + return operationQueue == null && _queueOperationTracker == null; } /// @@ -636,10 +1056,7 @@ public bool WasInitialized } /// Does this instance have any "queued" additions? - public bool HasQueuedOperations - { - get { return operationQueue != null && operationQueue.Count > 0; } - } + public bool HasQueuedOperations => operationQueue != null && operationQueue.Count > 0 || _queueOperationTracker?.HasChanges() == true; /// public IEnumerable QueuedAdditionIterator @@ -648,6 +1065,13 @@ public IEnumerable QueuedAdditionIterator { if (HasQueuedOperations) { + // Use the queue operation tracker when available to get the added elements as AdditionEnumerable + // does not work correctly when the item is added and then removed + if (_queueOperationTracker != null) + { + return _queueOperationTracker.GetAddedElements(); + } + return new AdditionEnumerable(this); } else @@ -661,20 +1085,35 @@ public ICollection GetQueuedOrphans(string entityName) { if (HasQueuedOperations) { - List additions = new List(operationQueue.Count); - List removals = new List(operationQueue.Count); - for (int i = 0; i < operationQueue.Count; i++) + List additions; + List removals; + + // Use the queue operation tracker when available to get the orphans as the default logic + // does not work correctly when readding a transient entity. Removals list should + // contain only entities that are already in the database. + if (_queueOperationTracker != null) { - IDelayedOperation op = operationQueue[i]; - if (op.AddedInstance != null) - { - additions.Add(op.AddedInstance); - } - if (op.Orphan != null) + removals = new List(_queueOperationTracker.GetOrphans().Cast()); + additions = new List(_queueOperationTracker.GetAddedElements().Cast()); + } + else // 6.0 TODO: Remove whole else block + { + additions = new List(operationQueue.Count); + removals = new List(operationQueue.Count); + for (int i = 0; i < operationQueue.Count; i++) { - removals.Add(op.Orphan); + var op = operationQueue[i]; + if (op.AddedInstance != null) + { + additions.Add(op.AddedInstance); + } + if (op.Orphan != null) + { + removals.Add(op.Orphan); + } } } + return GetOrphans(removals, additions, entityName, session); } @@ -704,12 +1143,7 @@ public virtual void AfterRowInsert(ICollectionPersister persister, object entry, /// protected virtual ICollection GetOrphans(ICollection oldElements, ICollection currentElements, string entityName, ISessionImplementor session) { - // short-circuit(s) - if (currentElements.Count == 0) - { - // no new elements, the old list contains only Orphans - return oldElements; - } + var collectionPersister = session.PersistenceContext.GetCollectionEntry(this).LoadedPersister as AbstractCollectionPersister; if (oldElements.Count == 0) { // no old elements, so no Orphans neither @@ -735,6 +1169,11 @@ protected virtual ICollection GetOrphans(ICollection oldElements, ICollection cu // iterate over the *old* list foreach (object old in oldElements) { + if (!IsOrphan(old, collectionPersister)) + { + continue; + } + object oldId = ForeignKeys.GetEntityIdentifierIfNotUnsaved(entityName, old, session); if (!currentIds.Contains(new TypedValue(idType, oldId, false))) { @@ -876,5 +1315,39 @@ public abstract object ReadFrom(DbDataReader reader, ICollectionPersister role, */ #endregion + + /// + /// Checks whether the element is actual an orphan or it was moved to a new parent. + /// + /// The element to check + /// The collection persister + /// Whether the element is an orphan or not. + private bool IsOrphan(object element, AbstractCollectionPersister collectionPersister) + { + if (collectionPersister?.ElementPersister == null) + { + return true; // Fallback to the old behavior if it is an unknown persister + } + + var entry = session.PersistenceContext.GetEntry(element); + if (entry == null) + { + return true; // The entity may not be loaded yet, can happen when using an instance from an another session + } + + if (!entry.ExistsInDatabase) + { + return false; + } + + var elementOwner = collectionPersister.GetElementOwner(element, session); + var ownerPersister = collectionPersister.OwnerEntityPersister; + if (elementOwner == null || ownerPersister.IdentifierType.IsEqual(ownerPersister.GetIdentifier(elementOwner), ownerPersister.GetIdentifier(Owner))) + { + return true; + } + + return false; // The element has moved to a new parent + } } } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs index 67564a5de3e..8c05e9bf8f9 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -48,7 +49,7 @@ public partial class PersistentGenericBag : AbstractPersistentCollection, ILi * ! */ private IList _gbag; - private bool _isOneToMany; + private bool _isOneToMany; // 6.0 TODO: Remove public PersistentGenericBag() { @@ -67,6 +68,19 @@ public PersistentGenericBag(ISessionImplementor session, IEnumerable coll) IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new BagQueueOperationTracker(entry.LoadedPersister); + } + + public override void ApplyQueuedOperations() + { + var queueOperation = (BagQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(_gbag); + QueueOperationTracker = null; + } + protected IList InternalBag { get { return _gbag; } @@ -119,15 +133,26 @@ int IList.IndexOf(object value) int IList.Add(object value) { - Add((T) value); + if (!IsOperationQueueEnabled || !ReadSize()) + { + Write(); + return ((IList) _gbag).Add((T) value); + } - //TODO: take a look at this - I don't like it because it changes the - // meaning of Add - instead of returning the index it was added at - // returns a "fake" index - not consistent with IList interface... - var count = !IsOperationQueueEnabled - ? _gbag.Count - : 0; - return count - 1; + var val = (T) value; + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElement(val); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleAddDelayedOperation(this, val)); +#pragma warning restore 618 + } + + return CachedSize - 1; } void IList.Insert(int index, object value) @@ -181,7 +206,17 @@ public void Add(T item) } else { - QueueOperation(new SimpleAddDelayedOperation(this, item)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElement(item); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleAddDelayedOperation(this, item)); +#pragma warning restore 618 + } } } @@ -189,7 +224,17 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueClearCollection(); + } + else + { +#pragma warning disable 618 + QueueOperation(new ClearDelayedOperation(this)); +#pragma warning restore 618 + } } else { @@ -204,7 +249,7 @@ public void Clear() public bool Contains(T item) { - return ReadElementExistence(item) ?? _gbag.Contains(item); + return ReadElementExistence(item, out _) ?? _gbag.Contains(item); } public void CopyTo(T[] array, int arrayIndex) @@ -501,6 +546,8 @@ private static int CountOccurrences(object element, IEnumerable list, IType elem return result; } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] private sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericBag _enclosingInstance; @@ -526,6 +573,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] private sealed class SimpleAddDelayedOperation : IDelayedOperation { private readonly PersistentGenericBag _enclosingInstance; diff --git a/src/NHibernate/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Collection/Generic/PersistentGenericList.cs index 8c676663f56..74ca1d4b24d 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericList.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -53,6 +54,11 @@ public PersistentGenericList(ISessionImplementor session, IList list) : base( IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new ListQueueOperationTracker(entry.LoadedPersister); + } public override object GetSnapshot(ICollectionPersister persister) { @@ -100,6 +106,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedList = (IList) persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (ListQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedList); + QueueOperationTracker = null; + } + public override bool IsWrapper(object collection) { return ReferenceEquals(WrappedList, collection); @@ -247,17 +260,26 @@ public override int GetHashCode() int IList.Add(object value) { - if (!IsOperationQueueEnabled) + if (!IsOperationQueueEnabled || !ReadSize()) { Write(); return ((IList)WrappedList).Add(value); } - QueueOperation(new SimpleAddDelayedOperation(this, (T) value)); - //TODO: take a look at this - I don't like it because it changes the - // meaning of Add - instead of returning the index it was added at - // returns a "fake" index - not consistent with IList interface... - return -1; + var val = (T) value; + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElement(val); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleAddDelayedOperation(this, val)); +#pragma warning restore 618 + } + + return CachedSize - 1; } bool IList.Contains(object value) @@ -269,7 +291,17 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueClearCollection(); + } + else + { +#pragma warning disable 618 + QueueOperation(new ClearDelayedOperation(this)); +#pragma warning restore 618 + } } else { @@ -301,17 +333,30 @@ public void RemoveAt(int index) { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object old = PutQueueEnabled ? ReadElementByIndex(index) : Unknown; - if (old == Unknown) + + var found = TryReadElementAtIndex(index, out var element); + if (!found.HasValue) { Write(); WrappedList.RemoveAt(index); } else { - QueueOperation(new RemoveDelayedOperation(this, index, old == NotFound ? null : old)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueRemoveElementAtIndex(index, element); + } + else + { +#pragma warning disable 618 + QueueOperation(new RemoveDelayedOperation(this, index, element)); +#pragma warning restore 618 + } } } @@ -346,7 +391,7 @@ public void Insert(int index, T item) { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException(nameof(index), "Index must be within the bounds of the List."); } if (!IsOperationQueueEnabled) { @@ -355,7 +400,17 @@ public void Insert(int index, T item) } else { - QueueOperation(new AddDelayedOperation(this, index, item)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElementAtIndex(index, item); + } + else + { +#pragma warning disable 618 + QueueOperation(new AddDelayedOperation(this, index, item)); +#pragma warning restore 618 + } } } @@ -365,39 +420,63 @@ public T this[int index] { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object result = ReadElementByIndex(index); - if (result == Unknown) + + var found = TryReadElementAtIndex(index, out var element); + if (!found.HasValue) { return WrappedList[index]; } - if (result == NotFound) + if (!found.Value) { // check if the index is valid if (index >= Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } return default(T); } - return (T) result; + return element; } set { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object old = PutQueueEnabled ? ReadElementByIndex(index) : Unknown; - if (old == Unknown) + + var old = default(T); + var found = PutQueueEnabled ? TryReadElementAtIndex(index, out old) : null; + if (!found.HasValue) { Write(); WrappedList[index] = value; } else { - QueueOperation(new SetDelayedOperation(this, index, value, old == NotFound ? null : old)); + if (EqualityComparer.Default.Equals(value, old)) + { + return; + } + + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueSetElementAtIndex(index, value, old); + } + else + { +#pragma warning disable 618 + QueueOperation(new SetDelayedOperation(this, index, value, old)); +#pragma warning restore 618 + } } } } @@ -450,13 +529,23 @@ public void Add(T item) } else { - QueueOperation(new SimpleAddDelayedOperation(this, item)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElement(item); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleAddDelayedOperation(this, item)); +#pragma warning restore 618 + } } } public bool Contains(T item) { - return ReadElementExistence(item) ?? WrappedList.Contains(item); + return ReadElementExistence(item, out _) ?? WrappedList.Contains(item); } public void CopyTo(T[] array, int arrayIndex) @@ -471,7 +560,8 @@ bool ICollection.IsReadOnly { public bool Remove(T item) { - bool? exists = PutQueueEnabled ? ReadElementExistence(item) : null; + bool? existsInDb = null; + bool? exists = PutQueueEnabled ? ReadElementExistence(item, out existsInDb) : null; if (!exists.HasValue) { Initialize(true); @@ -484,9 +574,21 @@ public bool Remove(T item) } else if (exists.Value) { - QueueOperation(new SimpleRemoveDelayedOperation(this, item)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueRemoveExistingElement(item, existsInDb); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleRemoveDelayedOperation(this, item)); +#pragma warning restore 618 + } + return true; } + return false; } @@ -530,6 +632,8 @@ IEnumerator IEnumerable.GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -555,6 +659,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleAddDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -582,6 +688,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class AddDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -611,6 +719,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SetDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -642,6 +752,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class RemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -671,6 +783,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleRemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; diff --git a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs index 994f71e0d44..add2345fae1 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -56,6 +57,12 @@ public PersistentGenericMap(ISessionImplementor session, IDictionary(entry.LoadedPersister); + } + public override object GetSnapshot(ICollectionPersister persister) { Dictionary clonedMap = new Dictionary(WrappedMap.Count); @@ -108,6 +115,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedMap = (IDictionary)persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (MapQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedMap); + QueueOperationTracker = null; + } + public override bool Empty { get { return (WrappedMap.Count == 0); } @@ -243,8 +257,7 @@ public override bool EntryExists(object entry, int i) public bool ContainsKey(TKey key) { - bool? exists = ReadIndexExistence(key); - return !exists.HasValue ? WrappedMap.ContainsKey(key) : exists.Value; + return ReadKeyExistence(key) ?? WrappedMap.ContainsKey(key); } public void Add(TKey key, TValue value) @@ -255,13 +268,30 @@ public void Add(TKey key, TValue value) } if (PutQueueEnabled) { - object old = ReadElementByIndex(key); - if (old != Unknown) + var found = TryReadElementByKey(key, out _, out _); + if (found.HasValue) { - QueueOperation(new PutDelayedOperation(this, key, value, old == NotFound ? null : old)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueAddElementByKey(key, value, found.Value); + } + else + { + if (found.Value) + { + throw new ArgumentException("An item with the same key has already been added."); // Mimic dictionary behavior + } + +#pragma warning disable 618 + QueueOperation(new PutDelayedOperation(this, key, value, default(TValue))); +#pragma warning restore 618 + } + return; } } + Initialize(true); WrappedMap.Add(key, value); Dirty(); @@ -269,8 +299,10 @@ public void Add(TKey key, TValue value) public bool Remove(TKey key) { - object old = PutQueueEnabled ? ReadElementByIndex(key) : Unknown; - if (old == Unknown) // queue is not enabled for 'puts', or element not found + var oldValue = default(TValue); + var existsInDb = default(bool?); + var found = PutQueueEnabled ? TryReadElementByKey(key, out oldValue, out existsInDb) : null; + if (!found.HasValue) // queue is not enabled for 'puts' or collection was initialized { Initialize(true); bool contained = WrappedMap.Remove(key); @@ -281,67 +313,91 @@ public bool Remove(TKey key) return contained; } - QueueOperation(new RemoveDelayedOperation(this, key, old == NotFound ? null : old)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + return QueueRemoveElementByKey(key, oldValue, existsInDb); + } + +#pragma warning disable 618 + QueueOperation(new RemoveDelayedOperation(this, key, oldValue)); +#pragma warning restore 618 return true; } public bool TryGetValue(TKey key, out TValue value) { - object result = ReadElementByIndex(key); - if (result == Unknown) + var found = TryReadElementByKey(key, out value, out _); + if (!found.HasValue) // collection was initialized { return WrappedMap.TryGetValue(key, out value); } - if(result == NotFound) + + if (found.Value) { - value = default(TValue); - return false; + return true; } - value = (TValue)result; - return true; + + value = default(TValue); + return false; } public TValue this[TKey key] { get { - object result = ReadElementByIndex(key); - if (result == Unknown) + var found = TryReadElementByKey(key, out var value, out _); + if (!found.HasValue) // collection was initialized { return WrappedMap[key]; } - if (result == NotFound) + + if (!found.Value) { throw new KeyNotFoundException(); } - return (TValue) result; + + return value; } set { // NH Note: the assignment in NET work like the put method in JAVA (mean assign or add) if (PutQueueEnabled) { - object old = ReadElementByIndex(key); - if (old != Unknown) + var found = TryReadElementByKey(key, out var oldValue, out var existsInDb); + if (found.HasValue) { - QueueOperation(new PutDelayedOperation(this, key, value, old == NotFound ? null : old)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueSetElementByKey(key, value, oldValue, existsInDb); + } + else + { +#pragma warning disable 618 + QueueOperation(new PutDelayedOperation(this, key, value, oldValue)); +#pragma warning restore 618 + } + return; } } + Initialize(true); - TValue tempObject; - WrappedMap.TryGetValue(key, out tempObject); - WrappedMap[key] = value; - TValue old2 = tempObject; - // would be better to use the element-type to determine - // whether the old and the new are equal here; the problem being - // we do not necessarily have access to the element type in all - // cases - if (!ReferenceEquals(value, old2)) + if (!WrappedMap.TryGetValue(key, out var old)) { + WrappedMap.Add(key, value); Dirty(); } + else + { + WrappedMap[key] = value; + if (!EqualityComparer.Default.Equals(value, old)) + { + Dirty(); + } + } } } @@ -375,7 +431,17 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueClearCollection(); + } + else + { +#pragma warning disable 618 + QueueOperation(new ClearDelayedOperation(this)); +#pragma warning restore 618 + } } else { @@ -390,7 +456,7 @@ public void Clear() public bool Contains(KeyValuePair item) { - bool? exists = ReadIndexExistence(item.Key); + bool? exists = ReadKeyExistence(item.Key); if (!exists.HasValue) { return WrappedMap.Contains(item); @@ -485,6 +551,8 @@ IEnumerator IEnumerable.GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; @@ -510,6 +578,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class PutDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; @@ -541,6 +611,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class RemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; diff --git a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs index 812f7c26ce5..135bfcb9816 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -74,6 +75,12 @@ public PersistentGenericSet(ISessionImplementor session, ISet original) IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new SetQueueOperationTracker(entry.LoadedPersister); + } + public override bool RowUpdatePossible { get { return false; } @@ -131,6 +138,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedSet = (ISet)persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (SetQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedSet); + QueueOperationTracker = null; + } + /// /// Initializes this PersistentSet from the cached values. /// @@ -295,33 +309,44 @@ public override bool IsWrapper(object collection) #region ISet Members - public bool Contains(T item) { - bool? exists = ReadElementExistence(item); - return exists == null ? WrappedSet.Contains(item) : exists.Value; + return ReadElementExistence(item, out _) ?? WrappedSet.Contains(item); } - public bool Add(T o) { - bool? exists = IsOperationQueueEnabled ? ReadElementExistence(o) : null; - if (!exists.HasValue) + // Skip checking the element existance in the database if we know that the element + // is transient and the operation queue is enabled + if (WasInitialized || !IsOperationQueueEnabled || !IsTransient(o)) { - Initialize(true); - if (WrappedSet.Add(o)) + var exists = IsOperationQueueEnabled ? ReadElementExistence(o, out _) : null; + if (!exists.HasValue) { - Dirty(); - return true; + Initialize(true); + if (WrappedSet.Add(o)) + { + Dirty(); + return true; + } + return false; + } + + if (exists.Value) + { + return false; } - return false; } - if (exists.Value) + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) { - return false; + return QueueAddElement(o); } + +#pragma warning disable 618 QueueOperation(new SimpleAddDelayedOperation(this, o)); +#pragma warning restore 618 return true; } @@ -425,7 +450,8 @@ public bool SetEquals(IEnumerable other) public bool Remove(T o) { - bool? exists = PutQueueEnabled ? ReadElementExistence(o) : null; + var existsInDb = default(bool?); + var exists = PutQueueEnabled ? ReadElementExistence(o, out existsInDb) : null; if (!exists.HasValue) { Initialize(true); @@ -439,9 +465,21 @@ public bool Remove(T o) if (exists.Value) { - QueueOperation(new SimpleRemoveDelayedOperation(this, o)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueRemoveExistingElement(o, existsInDb); + } + else + { +#pragma warning disable 618 + QueueOperation(new SimpleRemoveDelayedOperation(this, o)); +#pragma warning restore 618 + } + return true; } + return false; } @@ -449,7 +487,17 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker != null) + { + QueueClearCollection(); + } + else + { +#pragma warning disable 618 + QueueOperation(new ClearDelayedOperation(this)); +#pragma warning restore 618 + } } else { @@ -536,6 +584,8 @@ public IEnumerator GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; @@ -561,6 +611,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleAddDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; @@ -588,6 +640,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleRemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; @@ -617,4 +671,4 @@ public void Operate() #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs new file mode 100644 index 00000000000..6525ec31774 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + internal abstract class AbstractCollectionQueueOperationTracker : AbstractQueueOperationTracker + { + /// + /// A method that is called when an element is added to the collection. + /// + /// The element to add. + /// True whether the element was successfully added to the queue, false otherwise + public abstract bool AddElement(T element); + + /// + /// A method that is called when an existing element is removed from the collection. + /// + /// The element to remove. + /// Whether the element exists in the database. + public abstract void RemoveExistingElement(T element, bool? existsInDb); + + /// + /// Checks whether the element exists in the queue. + /// + /// The element to check. + /// True whether the element exists in the queue, false otherwise. + public abstract bool ContainsElement(T element); + + /// + /// Checks whether the element is queued for removal. + /// + /// The element to check. + /// True whether the element is queued for removal, false otherwise. + public abstract bool IsElementQueuedForDelete(T element); + + /// + public override void AfterElementFlushing(object element) + { + AfterElementFlushing((T) element); + } + + /// + /// A method that will be called when an element was flushed separately due to a special requirement + /// (e.g. saving an element with an id generator that requries an immediate insert). The queue should + /// ignore the flushed element when appending elements to the loaded collection in order to prevent duplicates. + /// + /// The element to evict. + protected internal virtual void AfterElementFlushing(T element) { } + + #region Indexed operations + + /// + /// A method that is called when an element is removed by its index from the collection. + /// + /// The index of the element. + /// The element to remove. + public abstract void RemoveElementAtIndex(int index, T element); + + /// + /// A method that is called when an element is added at a specific index of the collection. + /// + /// The index to put the element. + /// The element to add. + public abstract void AddElementAtIndex(int index, T element); + + /// + /// A method that is called when an element is set at a specific index of the collection. + /// + /// The index to set the new element. + /// The element to set. + /// The element that currently occupies the . + public abstract void SetElementAtIndex(int index, T element, T oldElement); + + /// + /// Tries to retrieve the element by a specific index of the collection. + /// + /// The index to put the element. + /// The output variable for the element. + /// True whether the element was found, false otherwise. + public abstract bool TryGetElementAtIndex(int index, out T element); + + /// + /// Calculates the element index where it currently lies in the database by taking into the consideration the queued operations. + /// + /// The effective index that will be when all operations would be flushed. + /// The element index in the database or -1 if the index represents a transient element. + public abstract int CalculateDatabaseElementIndex(int index); + + #endregion + } + + internal abstract class AbstractCollectionQueueOperationTracker : AbstractCollectionQueueOperationTracker + where TCollection : ICollection + { + /// + /// Applies all the queued changes to the loaded collection. + /// + /// The loaded collection. + public abstract void ApplyChanges(TCollection loadedCollection); + } +} diff --git a/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs new file mode 100644 index 00000000000..e0ddabca446 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + internal abstract class AbstractMapQueueOperationTracker : AbstractQueueOperationTracker + { + /// + /// Tries to retrieve a queued element by its key. + /// + /// The element key. + /// The output variable for the element. + /// True whether the element was found, false otherwise. + public abstract bool TryGetElementByKey(TKey elementKey, out TValue element); + + /// + /// Tries to retrieve a element that exists in the database by its key. + /// + /// The element key. + /// The output variable for the element. + /// True whether the element was found, false otherwise. + public abstract bool TryGetDatabaseElementByKey(TKey elementKey, out TValue element); + + /// + /// Checks whether the key exist in the queue. + /// + /// The key to check. + /// True whether it exists, false otherwise. + public abstract bool ContainsKey(TKey key); + + /// + /// A method that is called when the map method is called. + /// + /// The key to add. + /// The element to add + /// Whether the key exists in the database or in the queue. + public abstract void AddElementByKey(TKey elementKey, TValue element, bool exists); + + /// + /// A method that is called when the map is set. + /// + /// The key to set. + /// The element to set. + /// The element that currently occupies the . + /// Whether the element exists in the database. + public abstract void SetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb); + + /// + /// A method that is called when the map is called. + /// + /// The key to remove. + /// The element that currently occupies the . + /// Whether the element exists in the database. + /// True whether the key was successfully removed from the queue. + public abstract bool RemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb); + + /// + /// Checks whether the element key is queued for removal. + /// + /// The element key to check. + /// True whether the element key is queued for removal, false otherwise. + public abstract bool IsElementKeyQueuedForDelete(TKey elementKey); + + /// + /// Applies all the queued changes to the loaded map. + /// + /// The loaded map. + public abstract void ApplyChanges(IDictionary loadedMap); + } +} diff --git a/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs new file mode 100644 index 00000000000..2b204e363fe --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + /// A tracker that is able to track changes that are done to an uninitialized collection. + /// + internal abstract class AbstractQueueOperationTracker + { + internal static HashSet IndexOperations = new HashSet + { + nameof(AbstractCollectionQueueOperationTracker.RemoveElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.AddElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.SetElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.TryGetElementAtIndex) + }; + + /// + /// The number of elements that the collection have in the database. + /// + public virtual int DatabaseCollectionSize { get; protected internal set; } = -1; + + /// + /// Whether the Clear operation was performed on the uninitialized collection. + /// + public virtual bool Cleared { get; protected set; } + + /// + /// Retruns the current size of the queue that can be negative when there are more removed than added elements. + /// + /// The queue size. + public abstract int GetQueueSize(); + + /// + /// Returns the current size of the collection by taking into the consideration the queued operations. + /// + /// The current collection size. + public int GetCollectionSize() + { + if (Cleared) + { + return GetQueueSize(); + } + + if (DatabaseCollectionSize < 0) + { + throw new InvalidOperationException($"{nameof(DatabaseCollectionSize)} is not set"); + } + + return DatabaseCollectionSize + GetQueueSize(); + } + + /// + /// Checks whether the database collection size is required for the given operation. + /// + /// The operation name to check. + /// True whether the database collection size is required, false otherwise. + public virtual bool RequiresDatabaseCollectionSize(string operationName) => false; + + /// + /// Checks whether flushing is required for the given operation. + /// + /// The operation name to check. + /// True whether flushing is required, false otherwise. + public virtual bool RequiresFlushing(string operationName) => false; + + /// + /// A method that will be called once the flushing is done. + /// + public abstract void AfterFlushing(); + + /// + /// A method that will be called before an operation. + /// + /// The operation that will be executed. + public virtual void BeforeOperation(string operationName) { } + + /// + /// A method that will be called when a Clear operation is performed on the collection. + /// + public virtual void ClearCollection() + { + Cleared = true; + } + + /// + /// A method that will be called when an element was flushed separately due to a special requirement + /// (e.g. saving an element with an id generator that requries an immediate insert). The queue should + /// ignore the flushed element when appending elements to the loaded collection in order to prevent duplicates. + /// + /// The element to evict. + public abstract void AfterElementFlushing(object element); + + /// + /// Returns an of elements that were added into the collection. + /// + /// An of added elements. + public abstract IEnumerable GetAddedElements(); + + /// + /// Returns an of orphan elements of the collection. + /// + /// An of orphan elements. + public abstract IEnumerable GetOrphans(); + + /// + /// Checks whether a write operation was performed. + /// + /// True whether a write operation was performed, false otherwise. + public abstract bool HasChanges(); + } +} diff --git a/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs new file mode 100644 index 00000000000..a28600f5600 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class BagQueueOperationTracker : CollectionQueueOperationTracker> + { + public BagQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + + /// + public override void AfterFlushing() + { + // We have to reset the current database collection size in case an element + // was added multiple times + DatabaseCollectionSize = -1; + base.AfterFlushing(); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs new file mode 100644 index 00000000000..c4a26915683 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class ClearedListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + protected IList Collection; + private readonly ICollectionPersister _collectionPersister; + + public ClearedListQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + /// + public override bool AddElement(T element) + { + GetOrCreateQueue().Add(element); + return true; + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + Collection?.Remove(element); + } + + /// + public override bool Cleared + { + get => true; + protected set => throw new NotSupportedException(); + } + + public override void AfterFlushing() + { + throw new NotSupportedException(); + } + + /// + public override void ClearCollection() + { + Collection?.Clear(); + } + + /// + public override bool ContainsElement(T element) + { + return Collection?.Contains(element) ?? false; + } + + /// + public override int GetQueueSize() + { + return Collection?.Count ?? 0; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return false; + } + + /// + public override bool HasChanges() + { + return true; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + loadedCollection.Clear(); + if (Collection != null) + { + foreach (var toAdd in Collection) + { + loadedCollection.Add(toAdd); + } + } + } + + /// + public override int CalculateDatabaseElementIndex(int index) + { + return -1; + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + if (Collection == null || index < 0 || index >= Collection.Count) + { + element = default(T); + return false; + } + + element = Collection[index]; + return true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + Collection?.RemoveAt(index); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + GetOrCreateQueue().Insert(index, element); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + GetOrCreateQueue()[index] = element; + } + + /// + public override IEnumerable GetAddedElements() + { + return Collection ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return Enumerable.Empty(); + } + + private IList GetOrCreateQueue() + { + return Collection ?? (Collection = (IList) _collectionPersister.CollectionType.Instantiate(-1)); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs new file mode 100644 index 00000000000..91376c4f208 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + internal abstract class CollectionQueueOperationTracker : AbstractCollectionQueueOperationTracker + where TCollection : ICollection + { + protected TCollection Queue; + protected ISet RemovalQueue; + protected int QueueSize; + protected ISet Orphans; + protected internal ISet FlushedElements; + protected readonly ICollectionPersister CollectionPersister; + + protected CollectionQueueOperationTracker(ICollectionPersister collectionPersister) + { + CollectionPersister = collectionPersister; + } + + /// + public override bool AddElement(T element) + { + InitializeQueue(); + RemovalQueue?.Remove(element); // We have to remove the element from the removal list when the element is readded + if (FlushedElements?.Remove(element) == true) + { + return true; + } + + return Add(element); + } + + protected virtual bool Add(T element) + { + Queue.Add(element); + if (!Cleared) + { + QueueSize++; + } + + return true; + } + + /// + public override void AfterFlushing() + { + Queue?.Clear(); + QueueSize = 0; + Orphans?.Clear(); + RemovalQueue?.Clear(); + FlushedElements?.Clear(); + } + + /// + public override bool HasChanges() + { + return Cleared || RemovalQueue?.Count > 0 || Queue?.Count > 0; + } + + /// + protected internal override void AfterElementFlushing(T element) + { + if (Queue?.Remove(element) != true) + { + GetOrCreateFlushedElements().Add(element); + } + else + { + QueueSize--; + } + } + + /// + public override void ApplyChanges(TCollection loadedCollection) + { + if (Cleared) + { + loadedCollection.Clear(); + } + else if (RemovalQueue != null) + { + foreach (var toRemove in RemovalQueue) + { + loadedCollection.Remove(toRemove); + } + } + + if (Queue != null) + { + foreach (var toAdd in Queue) + { + loadedCollection.Add(toAdd); + } + } + } + + /// + public override bool RequiresFlushing(string operationName) + { + return IndexOperations.Contains(operationName); + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + if (!Cleared) + { + // As this method is called only when the element exists in the queue or in database, we can safely reduce the queue size + QueueSize--; + } + + if (existsInDb == true) + { + GetOrCreateOrphansSet().Add(element); + } + + GetOrCreateRemovalQueue().Add(element); + Queue?.Remove(element); + } + + /// + public override void ClearCollection() + { + AfterFlushing(); + Cleared = true; + } + + /// + public override bool ContainsElement(T element) + { + return Queue?.Contains(element) ?? false; + } + + /// + public override int GetQueueSize() + { + return Cleared ? (Queue?.Count ?? 0) : QueueSize; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return RemovalQueue?.Contains(element) ?? false; + } + + /// + public override IEnumerable GetAddedElements() + { + return (IEnumerable) Queue ?? Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return (IEnumerable) Orphans ?? Enumerable.Empty(); + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + throw new NotSupportedException(); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + throw new NotSupportedException(); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + throw new NotSupportedException(); + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + throw new NotSupportedException(); + } + + /// + public override int CalculateDatabaseElementIndex(int index) + { + throw new NotSupportedException(); + } + + protected ISet GetOrCreateRemovalQueue() + { + return RemovalQueue ?? (RemovalQueue = new HashSet()); + } + + protected ISet GetOrCreateOrphansSet() + { + return Orphans ?? (Orphans = new HashSet()); + } + + protected ISet GetOrCreateFlushedElements() + { + return FlushedElements ?? (FlushedElements = new HashSet()); + } + + private void InitializeQueue() + { + if (Queue == null) + { + Queue = (TCollection) CollectionPersister.CollectionType.Instantiate(-1); + } + } + } +} diff --git a/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs new file mode 100644 index 00000000000..3d0e466e399 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class IndexedListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + private static readonly KeyValuePairComparer Comparer = new KeyValuePairComparer(); + private class KeyValuePairComparer : IComparer> + { + public int Compare(KeyValuePair x, KeyValuePair y) + { + if (x.Key > y.Key) + { + return 1; + } + + if (x.Key < y.Key) + { + return -1; + } + + return 0; + } + } + + private List> _queue; // Sorted queue by index + private int _queueSize; + private List> _removedDbIndexes; // Sorted removed db indexes + private ISet _removalQueue; + private ISet _flushedElements; + + public IndexedListQueueOperationTracker(ISet flushedElements) + { + _flushedElements = flushedElements; + } + + /// + public override bool RequiresFlushing(string operationName) + { + // For remove operation we don't know the index of the removal item + return nameof(RemoveExistingElement) == operationName; + } + + /// + public override bool RequiresDatabaseCollectionSize(string operationName) + { + return IndexOperations.Contains(operationName) || nameof(AddElement) == operationName; + } + + /// + public override bool ContainsElement(T element) + { + if (_queue == null) + { + return false; + } + + foreach (var pair in _queue) + { + if (EqualityComparer.Default.Equals(pair.Value, element)) + { + return true; + } + } + + return false; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return _removalQueue?.Contains(element) ?? false; + } + + /// + public override bool HasChanges() + { + return _removedDbIndexes?.Count > 0 || _queue?.Count > 0; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + if (_removedDbIndexes != null) + { + for (var i = _removedDbIndexes.Count - 1; i >= 0; i--) + { + loadedCollection.RemoveAt(_removedDbIndexes[i].Key); + } + } + + if (_queue != null) + { + for (var i = 0; i < _queue.Count; i++) + { + var pair = _queue[i]; + loadedCollection.Insert(pair.Key, pair.Value); + } + } + } + + /// + public override bool AddElement(T element) + { + AddElementAtIndex(GetCollectionSize(), element); + return true; + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + throw new NotSupportedException(); + } + + /// + public override void ClearCollection() + { + throw new NotSupportedException(); + } + + /// + public override void AfterFlushing() + { + throw new NotSupportedException(); + } + + /// + public override IEnumerable GetAddedElements() + { + return _queue?.Select(o => o.Value) ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _removedDbIndexes?.Select(o => o.Value) ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override bool Cleared + { + get => false; + protected set => throw new NotSupportedException(); + } + + /// + public override int GetQueueSize() + { + return _queueSize; + } + + /// + protected internal override void AfterElementFlushing(T element) + { + var index = _queue?.FindIndex(pair => EqualityComparer.Default.Equals(pair.Value, element)); + if (!index.HasValue || index < 0) + { + GetOrCreateFlushedElements().Add(element); + } + else + { + _queue.RemoveAt(index.Value); + _queueSize--; + } + } + + /// + public override int CalculateDatabaseElementIndex(int index) + { + var dbIndex = index; + + if (_queue != null) + { + foreach (var pair in _queue) + { + if (pair.Key == index) + { + return -1; // The given index represents a queued value + } + + if (pair.Key > index) + { + break; + } + + dbIndex--; + } + } + + if (_removedDbIndexes != null) + { + var i = GetQueueIndex(ref _removedDbIndexes, new KeyValuePair(dbIndex, default(T)), true); + if (i < 0) + { + dbIndex += (Math.Abs(i) - 1); + } + else + { + i++; + dbIndex += i; + + // Increase until we find a non removed db index + for (; i < _removedDbIndexes.Count; i++) + { + if (_removedDbIndexes[i].Key != dbIndex) + { + break; + } + + dbIndex++; + } + } + } + + return dbIndex >= DatabaseCollectionSize + ? -1 + : dbIndex; + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + var pair = new KeyValuePair(index, default(T)); + var keyIndex = GetQueueIndex(ref _queue, pair, true); + if (keyIndex < 0) + { + element = default(T); + return false; + } + + element = _queue[keyIndex].Value; + return true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + if (index >= GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + + var pair = new KeyValuePair(index, default(T)); + var i = GetQueueIndex(ref _queue, pair, true); + if (i < 0) + { + i = Math.Abs(i) - 1; + var dbIndex = CalculateDatabaseElementIndex(index); + var removedPair = new KeyValuePair(dbIndex, element); + var j = GetQueueIndex(ref _removedDbIndexes, removedPair, true); + if (j < 0) + { + _removedDbIndexes.Insert(Math.Abs(j) - 1, removedPair); + } + } + else + { + _queue.RemoveAt(i); + } + + _queueSize--; + GetOrCreateRemovalQueue().Add(element); + + // We have to decrement all higher indexes by 1 + for (; i < _queue.Count; i++) + { + var currentPair = _queue[i]; + _queue[i] = new KeyValuePair(currentPair.Key - 1, currentPair.Value); + } + } + + /// + public override void AddElementAtIndex(int index, T element) + { + if (index > GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException(nameof(index), "Index must be within the bounds of the List."); + } + + if (_flushedElements?.Remove(element) == true) + { + return; + } + + var pair = new KeyValuePair(index, element); + var i = GetQueueIndex(ref _queue, pair, false); + _queue.Insert(i, pair); + i++; + _queueSize++; + _removalQueue?.Remove(element); + + // We have to increment all higher indexes by 1 + for (; i < _queue.Count; i++) + { + var currentPair = _queue[i]; + _queue[i] = new KeyValuePair(currentPair.Key + 1, currentPair.Value); + } + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + if (index >= GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + + var pair = new KeyValuePair(index, element); + + // NOTE: If we would need to have only items that are removed in the removal queue we would need + // to check the _queue if the item already exists as the item can be readded to a different index. + // As this scenario should rarely happen and we also know that ContainsElement will be called before + // IsElementQueuedForDelete, we skip the check here to have a better performance. + GetOrCreateRemovalQueue().Add(oldElement); + _removalQueue.Remove(element); + + var i = GetQueueIndex(ref _queue, pair, true); + if (i < 0) + { + var dbIndex = CalculateDatabaseElementIndex(index); + var removedPair = new KeyValuePair(dbIndex, oldElement); + var j = GetQueueIndex(ref _removedDbIndexes, removedPair, true); + if (j < 0) + { + _removedDbIndexes.Insert(Math.Abs(j) - 1, removedPair); + } + + _queue.Insert(Math.Abs(i) - 1, pair); + } + else + { + _queue[i] = pair; + } + } + + private static int GetQueueIndex(ref List> queue, KeyValuePair pair, bool rawResult) + { + return GetQueueIndex(ref queue, pair, rawResult, Comparer); + } + + private static int GetQueueIndex(ref List queue, TType item, bool rawResult, IComparer comparer) + { + if (queue == null) + { + queue = new List(); + } + + var i = queue.BinarySearch(item, comparer); + if (i < 0) + { + return rawResult ? i : Math.Abs(i) - 1; + } + + return i; + } + + private ISet GetOrCreateRemovalQueue() + { + return _removalQueue ?? (_removalQueue = new HashSet()); + } + + private ISet GetOrCreateFlushedElements() + { + return _flushedElements ?? (_flushedElements = new HashSet()); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs new file mode 100644 index 00000000000..aa7c10bba4a --- /dev/null +++ b/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs @@ -0,0 +1,207 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class ListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + private readonly ICollectionPersister _collectionPersister; + private AbstractCollectionQueueOperationTracker> _tracker; + private ISet _flushedElements; + + public ListQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + /// + public override int DatabaseCollectionSize + { + get => _tracker?.DatabaseCollectionSize ?? base.DatabaseCollectionSize; + protected internal set + { + base.DatabaseCollectionSize = value; + if (_tracker != null) + { + _tracker.DatabaseCollectionSize = value; + } + } + } + + /// + public override bool RequiresDatabaseCollectionSize(string operationName) + { + return _tracker?.RequiresDatabaseCollectionSize(operationName) ?? IndexOperations.Contains(operationName); + } + + /// + public override int GetQueueSize() + { + return _tracker?.GetQueueSize() ?? 0; + } + + /// + public override void ClearCollection() + { + // ClearedListQueueOperationTracker does not need the flushed elements as it will never + // use the database collection size to calculate the current size + _flushedElements = null; + _tracker = new ClearedListQueueOperationTracker(_collectionPersister); + Cleared = true; + } + + /// + public override IEnumerable GetAddedElements() + { + return _tracker?.GetAddedElements() ?? Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _tracker?.GetOrphans() ?? Enumerable.Empty(); + } + + /// + protected internal override void AfterElementFlushing(T element) + { + if (_tracker == null) + { + GetOrCreateFlushedElements().Add(element); + return; + } + + _tracker.AfterElementFlushing(element); + } + + /// + public override bool RequiresFlushing(string operationName) + { + return _tracker?.RequiresFlushing(operationName) == true; + } + + /// + public override void AfterFlushing() + { + // We have to reset the current database collection size in case an element + // was added multiple times + DatabaseCollectionSize = -1; + _tracker = null; + } + + public override void BeforeOperation(string operationName) + { + if (_tracker != null) + { + return; + } + + _tracker = IndexOperations.Contains(operationName) + ? (AbstractCollectionQueueOperationTracker>) new IndexedListQueueOperationTracker(_flushedElements) + { + DatabaseCollectionSize = DatabaseCollectionSize + } + : new NonIndexedListQueueOperationTracker(_collectionPersister) + { + DatabaseCollectionSize = DatabaseCollectionSize, + FlushedElements = _flushedElements + }; + } + + /// + public override bool AddElement(T element) + { + return GetOrCreateStrategy().AddElement(element); + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + GetOrCreateStrategy().RemoveExistingElement(element, existsInDb); + } + + /// + public override bool ContainsElement(T element) + { + return _tracker?.ContainsElement(element) == true; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return _tracker?.IsElementQueuedForDelete(element) == true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + GetOrCreateIndexedStrategy().RemoveElementAtIndex(index, element); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + GetOrCreateIndexedStrategy().AddElementAtIndex(index, element); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + GetOrCreateIndexedStrategy().SetElementAtIndex(index, element, oldElement); + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + if (_tracker != null) + { + return _tracker.TryGetElementAtIndex(index, out element); + } + + element = default(T); + return false; + } + + /// + public override int CalculateDatabaseElementIndex(int index) + { + return _tracker?.CalculateDatabaseElementIndex(index) ?? index; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + _tracker?.ApplyChanges(loadedCollection); + } + + private AbstractCollectionQueueOperationTracker> GetOrCreateStrategy() + { + return _tracker ?? (_tracker = new NonIndexedListQueueOperationTracker(_collectionPersister) + { + DatabaseCollectionSize = DatabaseCollectionSize, + FlushedElements = _flushedElements + }); + } + + private AbstractCollectionQueueOperationTracker> GetOrCreateIndexedStrategy() + { + return _tracker ?? (_tracker = new IndexedListQueueOperationTracker(_flushedElements) + { + DatabaseCollectionSize = DatabaseCollectionSize + }); + } + + protected ISet GetOrCreateFlushedElements() + { + return _flushedElements ?? (_flushedElements = new HashSet()); + } + + public override bool HasChanges() + { + return _tracker?.HasChanges() ?? false; + } + } +} diff --git a/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs new file mode 100644 index 00000000000..632c5384aec --- /dev/null +++ b/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class MapQueueOperationTracker : AbstractMapQueueOperationTracker + { + private readonly ICollectionPersister _collectionPersister; + private IDictionary _queue; + private IDictionary _orphanMap; + private ISet _removalQueue; + private int _queueSize; + private ISet _flushedElements; + + public MapQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + public override void AfterFlushing() + { + _queue?.Clear(); + _removalQueue?.Clear(); + _queueSize = 0; + _orphanMap = null; + } + + /// + public override void ClearCollection() + { + AfterFlushing(); + Cleared = true; + } + + /// + public override void AfterElementFlushing(object element) + { + var value = (TValue) element; + if (!TryGetKeyForValue(value, out var key)) + { + GetOrCreateFlushedElements().Add(value); + } + else + { + _queue.Remove(key); + _queueSize--; + } + } + + /// + public override IEnumerable GetAddedElements() + { + return _queue?.Values ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _orphanMap?.Values ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override void AddElementByKey(TKey elementKey, TValue element, bool exists) + { + if (_flushedElements?.Remove(element) == true) + { + return; + } + + if (exists) + { + throw new ArgumentException("An item with the same key has already been added."); // Mimic dictionary behavior + } + + _removalQueue?.Remove(elementKey); // We have to remove the key from the removal list when the element is readded + GetOrCreateQueue().Add(elementKey, element); + if (!Cleared) + { + _queueSize++; + } + } + + /// + public override bool ContainsKey(TKey key) + { + return _queue?.ContainsKey(key) == true; + } + + /// + public override int GetQueueSize() + { + return Cleared ? (_queue?.Count ?? 0) : _queueSize; + } + + /// + public override bool IsElementKeyQueuedForDelete(TKey elementKey) + { + return _removalQueue?.Contains(elementKey) ?? false; + } + + /// + public override bool RemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb) + { + if (Cleared) + { + return _queue?.Remove(elementKey) ?? false; + } + + // We can have the following scenarios: + // 1. remove a key that exists in db and it is not in the queue and removal queue (decrease queue size) + // 2. remove a key that exists in db and it is in the queue (decrease queue size) + // 3. remove a key that does not exist in db and it is not in the queue (don't decrease queue size) + // 4. remove a key that does not exist in db and it is in the queue (decrease queue size) + // 5. remove a key that exists in db and it is in the removal queue (don't decrease queue size) + + // If the key is not present in the database and in the queue, do nothing + if (existsInDb == false && _queue?.ContainsKey(elementKey) != true) + { + return false; + } + + if (existsInDb == true) + { + GetOrCreateOrphanMap()[elementKey] = oldElement; + } + + // We don't want to have non database keys in the removal queue + if (_queue?.Remove(elementKey) == true | + (_orphanMap?.ContainsKey(elementKey) == true && GetOrCreateRemovalQueue().Add(elementKey))) + { + _queueSize--; + return true; + } + + return false; + } + + /// + public override void SetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb) + { + if (Cleared) + { + GetOrCreateQueue()[elementKey] = element; + return; + } + + // We can have the following scenarios: + // 1. set a key that exists in db and it is not in the queue (don't increase queue size) + // 2. set a key that exists in db and it is in the queue (don't increase queue size) + // 3. set a key that does not exist in db and it is not in the queue (increase queue size) + // 4. set a key that does not exist in db and it is in the queue (don't increase queue size) + // 5. set a key that exists in db and it is in the removal queue (increase queue size) + if ((existsInDb == false && _queue?.ContainsKey(elementKey) != true) || _removalQueue?.Remove(elementKey) == true) + { + _queueSize++; + } + + if (existsInDb == true) + { + GetOrCreateOrphanMap()[elementKey] = oldElement; + } + + GetOrCreateQueue()[elementKey] = element; + } + + /// + public override bool TryGetElementByKey(TKey elementKey, out TValue element) + { + if (_queue == null) + { + element = default(TValue); + return false; + } + + return _queue.TryGetValue(elementKey, out element); + } + + /// + public override bool TryGetDatabaseElementByKey(TKey elementKey, out TValue element) + { + if (_orphanMap == null) + { + element = default(TValue); + return false; + } + + return _orphanMap.TryGetValue(elementKey, out element); + } + + /// + public override bool HasChanges() + { + return Cleared || _removalQueue?.Count > 0 || _queue?.Count > 0; + } + + /// + public override void ApplyChanges(IDictionary loadedMap) + { + if (Cleared) + { + loadedMap.Clear(); + } + else if (_removalQueue != null) + { + foreach (var toRemove in _removalQueue) + { + loadedMap.Remove(toRemove); + } + } + + if (_queue != null) + { + foreach (var toAdd in _queue) + { + loadedMap[toAdd.Key] = toAdd.Value; + } + } + } + + private bool TryGetKeyForValue(TValue value, out TKey key) + { + if (_queue == null) + { + key = default(TKey); + return false; + } + + foreach (var pair in _queue) + { + if (EqualityComparer.Default.Equals(pair.Value, value)) + { + key = pair.Key; + return true; + } + } + + key = default(TKey); + return false; + } + + private IDictionary GetOrCreateQueue() + { + return _queue ?? (_queue = (IDictionary) _collectionPersister.CollectionType.Instantiate(-1)); + } + + private IDictionary GetOrCreateOrphanMap() + { + return _orphanMap ?? (_orphanMap = (IDictionary) _collectionPersister.CollectionType.Instantiate(-1)); + } + + private ISet GetOrCreateRemovalQueue() + { + return _removalQueue ?? (_removalQueue = new HashSet()); + } + + private ISet GetOrCreateFlushedElements() + { + return _flushedElements ?? (_flushedElements = new HashSet()); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs new file mode 100644 index 00000000000..f8a61fe5234 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class NonIndexedListQueueOperationTracker : CollectionQueueOperationTracker> + { + public NonIndexedListQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + } +} diff --git a/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs new file mode 100644 index 00000000000..682d3b36ff1 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + /// A tracker that is able to track changes that are done to an uninitialized map. + /// + internal class SetQueueOperationTracker : CollectionQueueOperationTracker> + { + public SetQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + + protected override bool Add(T element) + { + if (!Queue.Add(element)) + { + return false; + } + + if (!Cleared) + { + QueueSize++; + } + + return true; + } + } +} diff --git a/src/NHibernate/Event/Default/AbstractFlushingEventListener.cs b/src/NHibernate/Event/Default/AbstractFlushingEventListener.cs index f9f968a4b2a..b0aee20b954 100644 --- a/src/NHibernate/Event/Default/AbstractFlushingEventListener.cs +++ b/src/NHibernate/Event/Default/AbstractFlushingEventListener.cs @@ -46,18 +46,18 @@ protected virtual void FlushEverythingToExecutions(FlushEvent @event) session.Interceptor.PreFlush((ICollection) persistenceContext.EntitiesByKey.Values); - PrepareEntityFlushes(session); - // we could move this inside if we wanted to - // tolerate collection initializations during - // collection dirty checking: - PrepareCollectionFlushes(session); - // now, any collections that are initialized - // inside this block do not get updated - they - // are ignored until the next flush - persistenceContext.Flushing = true; try { + PrepareEntityFlushes(session); + // we could move this inside if we wanted to + // tolerate collection initializations during + // collection dirty checking: + PrepareCollectionFlushes(session); + // now, any collections that are initialized + // inside this block do not get updated - they + // are ignored until the next flush + FlushEntities(@event); FlushCollections(session); } diff --git a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs index f4fe501ab2e..350a537fce9 100644 --- a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs +++ b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs @@ -1,12 +1,13 @@ using System; using System.Collections; - using NHibernate.Action; using NHibernate.Classic; +using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Id; using NHibernate.Impl; using NHibernate.Intercept; +using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; using NHibernate.Type; using Status=NHibernate.Engine.Status; @@ -260,6 +261,13 @@ protected virtual object PerformSaveOrReplicate(object entity, EntityKey key, IE key = source.GenerateEntityKey(id, persister); source.PersistenceContext.CheckUniqueness(key, entity); //source.getBatcher().executeBatch(); //found another way to ensure that all batched joined inserts have been executed + + // Update uninitialized collections that contain the inserted child (NH-739). We don't need to update the collections + // when doing a full flush as they will execute all queued actions at once. + if (!source.PersistenceContext.Flushing) + { + UpdateCollectionsQueues(source, persister, entity); + } } else { @@ -293,6 +301,54 @@ protected virtual object PerformSaveOrReplicate(object entity, EntityKey key, IE return id; } + private static void UpdateCollectionsQueues(ISessionImplementor source, IEntityPersister persister, object entity) + { + var roles = source.Factory.GetCollectionRolesByEntityParticipant(persister.EntityName); + if (roles == null) + { + return; + } + + foreach (var role in roles) + { + if (!(source.Factory.GetCollectionPersister(role) is AbstractCollectionPersister collectionPersister)) + { + continue; + } + + var ownerKey = collectionPersister.GetElementOwnerKey(entity, source); + if (ownerKey == null) + { + continue; + } + + var colKey = new CollectionKey(collectionPersister, ownerKey); + var collection = source.PersistenceContext.GetCollection(colKey); + if (collection == null || + collection.WasInitialized || + !(collection is AbstractPersistentCollection persistentCollection)) + { + continue; + } + + var queueTracker = persistentCollection.GetOrCreateQueueOperationTracker(); + if (queueTracker == null) + { + continue; + } + + // We have to reset the cached sizes in order to avoid having an incorrect value + // for ICollection.Count + persistentCollection.ResetCachedSize(); + queueTracker.DatabaseCollectionSize = -1; + queueTracker.AfterElementFlushing(entity); + if (persistentCollection.IsDirty && !persistentCollection.HasQueuedOperations) + { + persistentCollection.ClearDirty(); + } + } + } + private void MarkInterceptorDirty(object entity, IEntityPersister persister, IEventSource source) { if (persister.IsInstrumented) diff --git a/src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs b/src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs index bc78652ef1f..169bd904cf6 100644 --- a/src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs +++ b/src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs @@ -124,6 +124,7 @@ public abstract partial class AbstractCollectionPersister : ICollectionMetadata, private readonly IIdentifierGenerator identifierGenerator; private readonly IPropertyMapping elementPropertyMapping; private readonly IEntityPersister elementPersister; + private readonly ElementOwnerProperty[] _elementOwnerProperties; private readonly ICacheConcurrencyStrategy cache; private readonly CollectionType collectionType; private ICollectionInitializer initializer; @@ -163,6 +164,15 @@ public abstract partial class AbstractCollectionPersister : ICollectionMetadata, private static readonly INHibernateLogger log = NHibernateLogger.For(typeof (ICollectionPersister)); + private class ElementOwnerProperty + { + public IType Type { get; set; } + + public int PropertyIndex { get; set; } + + public ElementOwnerProperty ComponentProperty { get; set; } + } + public AbstractCollectionPersister(Mapping.Collection collection, ICacheConcurrencyStrategy cache, ISessionFactoryImplementor factory) { this.factory = factory; @@ -253,6 +263,46 @@ public AbstractCollectionPersister(Mapping.Collection collection, ICacheConcurre string _entityName = ((EntityType) elementType).GetAssociatedEntityName(); elementPersister = factory.GetEntityPersister(_entityName); // NativeSQL: collect element column and auto-aliases + if (elementPersister is IOuterJoinLoadable elementLoadable) + { + var ownerProperties = new ElementOwnerProperty[keyColumnNames.Length]; + for (var i = 0; i < elementLoadable.PropertyNames.Length; i++) + { + var columns = elementLoadable.GetPropertyColumnNames(i); + foreach (var column in columns) + { + var index = System.Array.FindIndex(keyColumnNames, t => t.Equals(column, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + continue; + } + + var type = elementLoadable.PropertyTypes[i]; + if (type is IAbstractComponentType componentType) + { + FillComponentRelatedProperties(componentType, elementLoadable, ownerProperties, i); + } + else + { + ownerProperties[index] = new ElementOwnerProperty + { + PropertyIndex = i, + Type = type + }; + } + } + } + + if (elementLoadable.IdentifierType is IAbstractComponentType idComponentType) + { + FillComponentRelatedProperties(idComponentType, elementLoadable, ownerProperties, -1); + } + + if (ownerProperties.All(o => o != null)) + { + _elementOwnerProperties = ownerProperties; + } + } } else { @@ -900,6 +950,113 @@ private string GetIndexCountExpression() return IndexColumnNames[0] ?? IndexFormulas[0]; } + public object GetElementOwner(object element, ISessionImplementor session) + { + if (_elementOwnerProperties == null) + { + return null; + } + + var values = new object[_elementOwnerProperties.Length]; + for (var i = 0; i < _elementOwnerProperties.Length; i++) + { + var ownerProperty = _elementOwnerProperties[i]; + var ownerValue = GetElementOwnerPropertyValue(element, ownerProperty, session); + if (ownerProperty.Type is ManyToOneType) + { + return ownerValue; + } + + values[i] = ownerValue; + } + + if (!CollectionType.UseLHSPrimaryKey) // property-ref + { + var propertyType = OwnerEntityPersister.GetPropertyType(CollectionType.LHSPropertyName); + if (propertyType is ComponentType propertyRefComponentType) + { + var ownerKey = propertyRefComponentType.Instantiate(); + propertyRefComponentType.SetPropertyValues(ownerKey, values); + return session.PersistenceContext.GetCollectionOwner(ownerKey, this); + } + + return values.Length == 1 ? session.PersistenceContext.GetCollectionOwner(values[0], this) : null; + } + + if (OwnerEntityPersister.IdentifierType is ComponentType identifierComponentType) + { + var ownerKey = identifierComponentType.Instantiate(); + identifierComponentType.SetPropertyValues(ownerKey, values); + return session.PersistenceContext.GetCollectionOwner(ownerKey, this); + } + + if (values.Length == 1 && values[0] != null) + { + return session.PersistenceContext.GetEntity(new EntityKey(values[0], OwnerEntityPersister)); + } + + return null; + } + + internal object GetElementOwnerKey(object element, ISessionImplementor session) + { + var owner = GetElementOwner(element, session); + return owner == null ? null : CollectionType.GetKeyOfOwner(owner, session); + } + + private object GetElementOwnerPropertyValue(object element, ElementOwnerProperty ownerProperty, ISessionImplementor session) + { + var componentProperty = ownerProperty.ComponentProperty; + if (componentProperty == null) + { + return ownerProperty.PropertyIndex < 0 + ? elementPersister.GetIdentifier(element) + : elementPersister.GetPropertyValue(element, ownerProperty.PropertyIndex); + } + + var componentType = (IAbstractComponentType) componentProperty.Type; + var component = componentProperty.PropertyIndex < 0 + ? elementPersister.GetIdentifier(element) + : elementPersister.GetPropertyValue(element, componentProperty.PropertyIndex); + + return componentType.GetPropertyValue(component, ownerProperty.PropertyIndex, session); + } + + private void FillComponentRelatedProperties(IAbstractComponentType componentType, IOuterJoinLoadable elementLoadable, ElementOwnerProperty[] ownerProperties, int propertyIndex) + { + var componentProperty = new ElementOwnerProperty + { + PropertyIndex = propertyIndex, + Type = propertyIndex < 0 ? elementLoadable.IdentifierType : elementLoadable.PropertyTypes[propertyIndex] + }; + + for (var i = 0; i < componentType.PropertyNames.Length; i++) + { + var propertyName = componentType.PropertyNames[i]; + var columns = elementLoadable.GetPropertyColumnNames( + propertyIndex < 0 + ? $"{EntityPersister.EntityID}.{propertyName}" + : $"{elementLoadable.PropertyNames[propertyIndex]}.{propertyName}"); + foreach (var column in columns) + { + var index = System.Array.FindIndex(keyColumnNames, t => t.Equals(column, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + continue; + } + + var subProperty = new ElementOwnerProperty + { + ComponentProperty = componentProperty, + PropertyIndex = i, + Type = componentType.Subtypes[i] + }; + + ownerProperties[index] = subProperty; + } + } + } + private SqlString GenerateDetectRowByIndexString() { if (!hasIndex)