diff --git a/src/NHibernate.Test/Async/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs b/src/NHibernate.Test/Async/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs index 5ba2f200fbb..7aa992c5c4d 100644 --- a/src/NHibernate.Test/Async/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs +++ b/src/NHibernate.Test/Async/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs @@ -1234,7 +1234,6 @@ public virtual async Task OneToManyCollectionOptimisticLockingWithUpdateAsync() s = OpenSession(); t = s.BeginTransaction(); - c = await (s.CreateCriteria().UniqueResultAsync()); // If the entity uses a join mapping, DML queries require temp tables. if (Dialect.SupportsTemporaryTables) await (s.CreateQuery("delete from Party").ExecuteUpdateAsync()); @@ -1251,7 +1250,7 @@ public virtual async Task OneToManyCollectionOptimisticLockingWithUpdateAsync() await (s.DeleteAsync(partyOrig)); await (s.DeleteAsync(newParty)); } - + c = await (s.CreateCriteria().UniqueResultAsync()); await (s.DeleteAsync(c)); Assert.That(await (s.CreateCriteria().SetProjection(Projections.RowCountInt64()).UniqueResultAsync()), Is.EqualTo(0L)); Assert.That(await (s.CreateCriteria().SetProjection(Projections.RowCountInt64()).UniqueResultAsync()), Is.EqualTo(0L)); diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs new file mode 100644 index 00000000000..89586bfd9fb --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + using System.Threading.Tasks; + [TestFixture] + public class CircularReferenceFetchDepth0FixtureAsync : BaseFetchFixture + { + private int _id2; + + public CircularReferenceFetchDepth0FixtureAsync() : base(0) + { + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("max_fetch_depth", "0"); + base.Configure(configuration); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + _id2 = _id; + //Generate another test entity + base.OnSetUp(); + } + + [Test] + public async Task QueryOverAsync() + { + using (var session = OpenSession()) + { + Entity e1 = null; + Entity e2 = null; + var result = await (session.QueryOver(() => e1) + .JoinEntityAlias(() => e2, () => e2.EntityNumber == e1.EntityNumber && e2.EntityId != _id) + .Where(e => e.EntityId == _id).SingleOrDefaultAsync()); + + VerifyChildrenNotInitialized(result); + VerifyChildrenNotInitialized(await (session.LoadAsync(_id2))); + } + } + + [Test] + public async Task GetAsync() + { + using (var session = OpenSession()) + { + var result = await (session.GetAsync(_id)); + VerifyChildrenNotInitialized(result); + } + } + } +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs new file mode 100644 index 00000000000..5687c0cac44 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + using System.Threading.Tasks; + [TestFixture(1)] + [TestFixture(2)] + public class CircularReferenceFetchDepthFixtureAsync : BaseFetchFixture + { + private int _id2; + private int _id3; + + public CircularReferenceFetchDepthFixtureAsync(int depth) : base(depth) + { + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("max_fetch_depth", _depth.ToString()); + base.Configure(configuration); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + _id2 = _id; + + //Generate another test entities + base.OnSetUp(); + _id3 = _id; + base.OnSetUp(); + } + + [Test] + public async Task QueryOverAsync() + { + using (var session = OpenSession()) + { + Entity e1 = null; + Entity e2 = null; + Entity e3 = null; + var result = await (session.QueryOver(() => e1) + .JoinEntityAlias(() => e2, () => e2.EntityNumber == e1.EntityNumber && e2.EntityId == _id2) + .JoinEntityAlias(() => e3, () => e3.EntityNumber == e1.EntityNumber && e3.EntityId == _id3) + .Where(e => e.EntityId == _id).SingleOrDefaultAsync()); + + Verify(result); + + Verify(await (session.LoadAsync(_id2))); + Verify(await (session.LoadAsync(_id3))); + } + } + + [Test] + public async Task GetAsync() + { + using (var session = OpenSession()) + { + var result = await (session.GetAsync(_id)); + Verify(result); + } + } + } +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs new file mode 100644 index 00000000000..97e12483191 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs @@ -0,0 +1,47 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + using System.Threading.Tasks; + [TestFixture] + public class CircularReferenceFetchFixtureAsync : BaseFetchFixture + { + public CircularReferenceFetchFixtureAsync() : base(-1) + { + } + + [Test] + public async Task QueryOverAsync() + { + using (var session = OpenSession()) + { + var result = await (session.QueryOver().Where(e => e.EntityNumber == "Bob").SingleOrDefaultAsync()); + + Verify(result); + } + } + + [Test] + public async Task GetAsync() + { + using (var session = OpenSession()) + { + var result = await (session.GetAsync(_id)); + + Verify(result); + } + } + } +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs new file mode 100644 index 00000000000..4beeb767da4 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NUnit.Framework; +using NHCfg = NHibernate.Cfg; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + using System.Threading.Tasks; + [TestFixture] + public class DetectFetchLoopsFalseFixtureAsync : BugTestCase + { + protected override void Configure(NHCfg.Configuration configuration) + { + configuration.SetProperty(NHCfg.Environment.GenerateStatistics, "true"); + configuration.SetProperty(NHCfg.Environment.DetectFetchLoops, "false"); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from Person"); + + tx.Commit(); + } + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + string[] names = { "Alice", "Bob" }; + + for (int i = 0; i < names.Length; i++) + { + var name = names[i]; + + var parent = new Person() + { + Name = name, + Details = new Detail() + { + Data = $"Details for ${name}" + } + }; + + for (int j = 1; j <= 3; j++) + { + var child = new Person() + { + Name = $"Child ${j} of ${parent.Name}", + Parent = parent, + Details = new Detail() + { + Data = $"Details for child ${j} of ${name}" + } + }; + + parent.Children.Add(child); + } + + s.Save(parent); + } + + tx.Commit(); + } + } + + [Test] + public async Task QueryOverPersonWithParentAsync() + { + var stats = Sfi.Statistics; + + stats.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var people = await (s.QueryOver() + .Fetch(SelectMode.Fetch, p => p.Parent) + .Where(p => p.Parent != null) + .ListAsync()); + + foreach (Person p in people) + { + Assert.That(p.Parent, Is.Not.Null); + Assert.That(p.Parent.Details, Is.Not.Null); + } + + Assert.That(people.Count, Is.EqualTo(6)); + Assert.That(stats.QueryExecutionCount, Is.EqualTo(1)); + Assert.That(stats.EntityFetchCount, Is.EqualTo(0)); + Assert.That(stats.EntityLoadCount, Is.EqualTo(16)); + } + } + + [Test] + public async Task QueryOverSinglePersonWithParentAsync() + { + var stats = Sfi.Statistics; + + stats.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.QueryOver() + .Where(p => p.Parent != null) + .Fetch(SelectMode.Fetch, p => p.Parent) + .Take(1) + .SingleOrDefaultAsync()); + + Assert.That(person, Is.Not.Null); + Assert.That(stats.QueryExecutionCount, Is.EqualTo(1)); + Assert.That(stats.EntityFetchCount, Is.EqualTo(0)); + Assert.That(stats.EntityLoadCount, Is.EqualTo(4)); + } + } + } +} diff --git a/src/NHibernate.Test/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs b/src/NHibernate.Test/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs index 012e55c5a3c..71ed408923c 100644 --- a/src/NHibernate.Test/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs +++ b/src/NHibernate.Test/Immutable/EntityWithMutableCollection/AbstractEntityWithOneToManyTest.cs @@ -1223,7 +1223,6 @@ public virtual void OneToManyCollectionOptimisticLockingWithUpdate() s = OpenSession(); t = s.BeginTransaction(); - c = s.CreateCriteria().UniqueResult(); // If the entity uses a join mapping, DML queries require temp tables. if (Dialect.SupportsTemporaryTables) s.CreateQuery("delete from Party").ExecuteUpdate(); @@ -1240,7 +1239,7 @@ public virtual void OneToManyCollectionOptimisticLockingWithUpdate() s.Delete(partyOrig); s.Delete(newParty); } - + c = s.CreateCriteria().UniqueResult(); s.Delete(c); Assert.That(s.CreateCriteria().SetProjection(Projections.RowCountInt64()).UniqueResult(), Is.EqualTo(0L)); Assert.That(s.CreateCriteria().SetProjection(Projections.RowCountInt64()).UniqueResult(), Is.EqualTo(0L)); diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/BaseFetchFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/BaseFetchFixture.cs new file mode 100644 index 00000000000..8febf0ad25c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/BaseFetchFixture.cs @@ -0,0 +1,168 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + public abstract class BaseFetchFixture : TestCaseMappingByCode + { + protected int _id; + protected int _depth; + + protected BaseFetchFixture(int depth) + { + _depth = depth < 0 ? int.MaxValue : depth; + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(m => + { + m.Id(c => c.EntityId, id => + { + id.Generator(Generators.Native); + }); + m.Property(c => c.EntityNumber); + m.ManyToOne(c => c.ReferencedEntity, p => + { + p.Column("ReferencedEntityId"); + p.Fetch(FetchKind.Join); + p.Cascade(Mapping.ByCode.Cascade.Persist); + p.ForeignKey("none"); + }); + m.ManyToOne(c => c.AdditionalEntity, p => + { + p.Column("AdditionalEntityId"); + p.Fetch(FetchKind.Join); + p.Cascade(Mapping.ByCode.Cascade.Persist); + p.ForeignKey("none"); + }); + m.ManyToOne(c => c.SourceEntity, p => + { + p.Column("SourceEntityId"); + p.Fetch(FetchKind.Join); + p.Cascade(Mapping.ByCode.Cascade.Persist); + p.ForeignKey("none"); + }); + m.ManyToOne(c => c.Level1, p => + { + p.Column("Level1Id"); + p.Fetch(FetchKind.Join); + p.Cascade(Mapping.ByCode.Cascade.Persist); + p.ForeignKey("none"); + }); + }); + + mapper.Class(m => + { + m.Id(c => c.Id, id => + { + id.Generator(Generators.Native); + }); + m.Property(c => c.Name); + m.ManyToOne(c => c.Level2, p => + { + p.Column("Level2Id"); + p.Fetch(FetchKind.Join); + p.Cascade(Mapping.ByCode.Cascade.Persist); + p.ForeignKey("none"); + }); + }); + + mapper.Class(m => + { + m.Id(c => c.Id, id => + { + id.Generator(Generators.Native); + }); + m.Property(c => c.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var e1 = new Entity + { + EntityNumber = "Bob", + AdditionalEntity = CreateEntity(), + ReferencedEntity = CreateEntity(), + SourceEntity = CreateEntity(), + Level1 = new Level1Entity() { Level2 = new Level2Entity() } + }; + session.Save(e1); + transaction.Commit(); + _id = e1.EntityId; + } + } + private static Entity CreateEntity() + { + return new Entity + { + SourceEntity = new Entity(), + AdditionalEntity = new Entity(), + ReferencedEntity = new Entity(), + Level1 = new Level1Entity() { Level2 = new Level2Entity() } + }; + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + } + + protected void Verify(Entity result) + { + VerifyChildrenInitialized(result); + + VerifyChildrenNotInitialized(result.AdditionalEntity); + VerifyChildrenNotInitialized(result.SourceEntity); + VerifyChildrenNotInitialized(result.ReferencedEntity); + } + + protected void VerifyChildrenInitialized(Entity result) + { + var isInited = _depth > 0 ? (NUnit.Framework.Constraints.IResolveConstraint) Is.True : Is.False; + Assert.That(result, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result), isInited); + Assert.That(result.AdditionalEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.AdditionalEntity), isInited); + Assert.That(result.SourceEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.SourceEntity), isInited); + Assert.That(result.ReferencedEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.ReferencedEntity), isInited); + Assert.That(result.Level1, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Level1), isInited); + if (_depth >= 1) + { + Assert.That(result.Level1.Level2, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Level1.Level2), _depth > 1 ? Is.True : Is.False); + } + } + + protected static void VerifyChildrenNotInitialized(Entity result) + { + Assert.That(result, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result), Is.True); + Assert.That(result.AdditionalEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.AdditionalEntity), Is.False); + Assert.That(result.SourceEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.SourceEntity), Is.False); + Assert.That(result.ReferencedEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.ReferencedEntity), Is.False); + Assert.That(result.Level1, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Level1), Is.False); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs new file mode 100644 index 00000000000..d32bf89a58a --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepth0Fixture.cs @@ -0,0 +1,55 @@ +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + [TestFixture] + public class CircularReferenceFetchDepth0Fixture : BaseFetchFixture + { + private int _id2; + + public CircularReferenceFetchDepth0Fixture() : base(0) + { + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("max_fetch_depth", "0"); + base.Configure(configuration); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + _id2 = _id; + //Generate another test entity + base.OnSetUp(); + } + + [Test] + public void QueryOver() + { + using (var session = OpenSession()) + { + Entity e1 = null; + Entity e2 = null; + var result = session.QueryOver(() => e1) + .JoinEntityAlias(() => e2, () => e2.EntityNumber == e1.EntityNumber && e2.EntityId != _id) + .Where(e => e.EntityId == _id).SingleOrDefault(); + + VerifyChildrenNotInitialized(result); + VerifyChildrenNotInitialized(session.Load(_id2)); + } + } + + [Test] + public void Get() + { + using (var session = OpenSession()) + { + var result = session.Get(_id); + VerifyChildrenNotInitialized(result); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs new file mode 100644 index 00000000000..68f834e4c02 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchDepthFixture.cs @@ -0,0 +1,64 @@ +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + [TestFixture(1)] + [TestFixture(2)] + public class CircularReferenceFetchDepthFixture : BaseFetchFixture + { + private int _id2; + private int _id3; + + public CircularReferenceFetchDepthFixture(int depth) : base(depth) + { + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty("max_fetch_depth", _depth.ToString()); + base.Configure(configuration); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + _id2 = _id; + + //Generate another test entities + base.OnSetUp(); + _id3 = _id; + base.OnSetUp(); + } + + [Test] + public void QueryOver() + { + using (var session = OpenSession()) + { + Entity e1 = null; + Entity e2 = null; + Entity e3 = null; + var result = session.QueryOver(() => e1) + .JoinEntityAlias(() => e2, () => e2.EntityNumber == e1.EntityNumber && e2.EntityId == _id2) + .JoinEntityAlias(() => e3, () => e3.EntityNumber == e1.EntityNumber && e3.EntityId == _id3) + .Where(e => e.EntityId == _id).SingleOrDefault(); + + Verify(result); + + Verify(session.Load(_id2)); + Verify(session.Load(_id3)); + } + } + + [Test] + public void Get() + { + using (var session = OpenSession()) + { + var result = session.Get(_id); + Verify(result); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs new file mode 100644 index 00000000000..ef9b05969e4 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/CircularReferenceFetchFixture.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + [TestFixture] + public class CircularReferenceFetchFixture : BaseFetchFixture + { + public CircularReferenceFetchFixture() : base(-1) + { + } + + [Test] + public void QueryOver() + { + using (var session = OpenSession()) + { + var result = session.QueryOver().Where(e => e.EntityNumber == "Bob").SingleOrDefault(); + + Verify(result); + } + } + + [Test] + public void Get() + { + using (var session = OpenSession()) + { + var result = session.Get(_id); + + Verify(result); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/Detail.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/Detail.cs new file mode 100644 index 00000000000..9b4c3bf1c33 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/Detail.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + public class Detail + { + public virtual int Id { get; protected set; } + public virtual Person Person { get; set; } + public virtual string Data { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs new file mode 100644 index 00000000000..c1f48b44b73 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/DetectFetchLoopsFalseFixture.cs @@ -0,0 +1,119 @@ +using NUnit.Framework; +using NHCfg = NHibernate.Cfg; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + [TestFixture] + public class DetectFetchLoopsFalseFixture : BugTestCase + { + protected override void Configure(NHCfg.Configuration configuration) + { + configuration.SetProperty(NHCfg.Environment.GenerateStatistics, "true"); + configuration.SetProperty(NHCfg.Environment.DetectFetchLoops, "false"); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from Person"); + + tx.Commit(); + } + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + string[] names = { "Alice", "Bob" }; + + for (int i = 0; i < names.Length; i++) + { + var name = names[i]; + + var parent = new Person() + { + Name = name, + Details = new Detail() + { + Data = $"Details for ${name}" + } + }; + + for (int j = 1; j <= 3; j++) + { + var child = new Person() + { + Name = $"Child ${j} of ${parent.Name}", + Parent = parent, + Details = new Detail() + { + Data = $"Details for child ${j} of ${name}" + } + }; + + parent.Children.Add(child); + } + + s.Save(parent); + } + + tx.Commit(); + } + } + + [Test] + public void QueryOverPersonWithParent() + { + var stats = Sfi.Statistics; + + stats.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var people = s.QueryOver() + .Fetch(SelectMode.Fetch, p => p.Parent) + .Where(p => p.Parent != null) + .List(); + + foreach (Person p in people) + { + Assert.That(p.Parent, Is.Not.Null); + Assert.That(p.Parent.Details, Is.Not.Null); + } + + Assert.That(people.Count, Is.EqualTo(6)); + Assert.That(stats.QueryExecutionCount, Is.EqualTo(1)); + Assert.That(stats.EntityFetchCount, Is.EqualTo(0)); + Assert.That(stats.EntityLoadCount, Is.EqualTo(16)); + } + } + + [Test] + public void QueryOverSinglePersonWithParent() + { + var stats = Sfi.Statistics; + + stats.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.QueryOver() + .Where(p => p.Parent != null) + .Fetch(SelectMode.Fetch, p => p.Parent) + .Take(1) + .SingleOrDefault(); + + Assert.That(person, Is.Not.Null); + Assert.That(stats.QueryExecutionCount, Is.EqualTo(1)); + Assert.That(stats.EntityFetchCount, Is.EqualTo(0)); + Assert.That(stats.EntityLoadCount, Is.EqualTo(4)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/Entity.cs new file mode 100644 index 00000000000..fe086dbbe2b --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/Entity.cs @@ -0,0 +1,25 @@ +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + public class Entity + { + public virtual int EntityId { get; set; } + public virtual string EntityNumber { get; set; } + public virtual Entity ReferencedEntity { get; set; } + public virtual Entity AdditionalEntity { get; set; } + public virtual Entity SourceEntity { get; set; } + public virtual Level1Entity Level1 { get; set; } + } + + public class Level1Entity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual Level2Entity Level2 { get; set; } + } + + public class Level2Entity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH2201/Mappings.hbm.xml new file mode 100644 index 00000000000..04cabf557ba --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/Mappings.hbm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + Person + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/Order.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/Order.cs new file mode 100644 index 00000000000..00c4023ac2a --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/Order.cs @@ -0,0 +1,11 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + public class Order + { + public virtual int Id { get; set; } + public virtual Person Person { get; set; } + public virtual DateTime Date { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2201/Person.cs b/src/NHibernate.Test/NHSpecificTest/GH2201/Person.cs new file mode 100644 index 00000000000..619fe916647 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2201/Person.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH2201 +{ + public class Person + { + private Detail _detail; + + public Person() + { + this.Children = new HashSet(); + } + + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual Person Parent { get; set; } + public virtual ISet Children { get; protected set; } + public virtual Detail Details + { + get { return _detail; } + set + { + _detail = value; + + if (_detail != null) + { + _detail.Person = this; + } + } + } + } +} diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs index d885f5393a4..cc107802524 100644 --- a/src/NHibernate/Cfg/Environment.cs +++ b/src/NHibernate/Cfg/Environment.cs @@ -110,6 +110,12 @@ public static string Version public const string CurrentSessionContextClass = "current_session_context_class"; public const string UseSqlComments = "use_sql_comments"; + /// + /// Enable or disable the ability to detect loops in query fetches. + /// The default is to detect and elimate potential fetch loops. + /// + public const string DetectFetchLoops = "detect_fetch_loops"; + /// Enable formatting of SQL logged to the console public const string FormatSql = "format_sql"; diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs index 38c7ed31fdf..633133450a8 100644 --- a/src/NHibernate/Cfg/Settings.cs +++ b/src/NHibernate/Cfg/Settings.cs @@ -39,6 +39,7 @@ public Settings() public SqlStatementLogger SqlStatementLogger { get; internal set; } public int MaximumFetchDepth { get; internal set; } + public bool DetectFetchLoops { get; internal set; } public IDictionary QuerySubstitutions { get; internal set; } diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs index 02da6aa2ab5..4babe5afb93 100644 --- a/src/NHibernate/Cfg/SettingsFactory.cs +++ b/src/NHibernate/Cfg/SettingsFactory.cs @@ -95,6 +95,10 @@ public Settings BuildSettings(IDictionary properties) log.Info("Maximum outer join fetch depth: {0}", maxFetchDepth); } + bool detectFetchLoops = PropertiesHelper.GetBoolean(Environment.DetectFetchLoops, properties, true); + log.Info("Detect fetch loops: {0}", EnabledDisabled(detectFetchLoops)); + settings.DetectFetchLoops = detectFetchLoops; + IConnectionProvider connectionProvider = ConnectionProviderFactory.NewConnectionProvider(properties); ITransactionFactory transactionFactory = CreateTransactionFactory(properties); // TransactionManagerLookup transactionManagerLookup = TransactionManagerLookupFactory.GetTransactionManagerLookup( properties ); diff --git a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs index 5fa438cc85a..4eb40d53210 100644 --- a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs +++ b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs @@ -31,6 +31,7 @@ public AbstractEntityJoinWalker(string rootSqlAlias, IOuterJoinLoadable persiste protected virtual void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode) { AddAssociations(); + ProcessJoins(); IList allAssociations = new List(associations); var rootAssociation = CreateRootAssociation(); allAssociations.Add(rootAssociation); diff --git a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs index 107163b7b68..ee609e733b4 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs @@ -86,15 +86,16 @@ protected override void AddAssociations() AddExplicitEntityJoinAssociation(persister, tableAlias, translator.GetJoinType(criteriaPath, criteriaPathAlias), criteriaPath, criteriaPathAlias); IncludeInResultIfNeeded(persister, entityJoinInfo.Criteria, tableAlias, criteriaPath); //collect mapped associations for entity join - WalkEntityTree(persister, tableAlias, criteriaPath, 1); + WalkEntityTree(persister, tableAlias, criteriaPath); + ProcessJoins(); } } - protected override void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth) + protected override void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path) { // NH different behavior (NH-1476, NH-1760, NH-1785) - base.WalkEntityTree(persister, alias, path, currentDepth); - WalkCompositeComponentIdTree(persister, alias, path, currentDepth); + base.WalkEntityTree(persister, alias, path); + WalkCompositeComponentIdTree(persister, alias, path); } protected override OuterJoinableAssociation CreateRootAssociation() @@ -130,14 +131,14 @@ protected override ISet GetEntityFetchLazyProperties(string path) return translator.RootCriteria.GetEntityFetchLazyProperties(path); } - private void WalkCompositeComponentIdTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth) + private void WalkCompositeComponentIdTree(IOuterJoinLoadable persister, string alias, string path) { IType type = persister.IdentifierType; string propertyName = persister.IdentifierPropertyName; if (type != null && type.IsComponentType) { ILhsAssociationTypeSqlInfo associationTypeSQLInfo = JoinHelper.GetIdLhsSqlInfo(alias, persister, Factory); - WalkComponentTree((IAbstractComponentType) type, 0, alias, SubPath(path, propertyName), currentDepth, associationTypeSQLInfo); + WalkComponentTree((IAbstractComponentType) type, 0, alias, SubPath(path, propertyName), associationTypeSQLInfo); } } diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs index bc51a1c593f..731ee2cc670 100644 --- a/src/NHibernate/Loader/JoinWalker.cs +++ b/src/NHibernate/Loader/JoinWalker.cs @@ -31,6 +31,8 @@ public class JoinWalker private string[] aliases; private LockMode[] lockModeArray; private SqlString sql; + private readonly Queue _joinQueue = new(); + private int _depth; public string[] CollectionSuffixes { @@ -136,11 +138,11 @@ protected JoinWalker(ISessionFactoryImplementor factory, IDictionary private void AddAssociationToJoinTreeIfNecessary(IAssociationType type, string[] aliasedLhsColumns, - string alias, string path, string pathAlias, int currentDepth, JoinType joinType) + string alias, string path, string pathAlias, JoinType joinType) { if (joinType >= JoinType.InnerJoin) { - AddAssociationToJoinTree(type, aliasedLhsColumns, alias, path, pathAlias, currentDepth, joinType); + AddAssociationToJoinTree(type, aliasedLhsColumns, alias, path, pathAlias, joinType); } } @@ -164,7 +166,7 @@ protected virtual SqlString GetWithClause(string path, string pathAlias) /// of associations to be fetched by outerjoin /// private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhsColumns, string alias, - string path, string pathAlias, int currentDepth, JoinType joinType) + string path, string pathAlias, JoinType joinType) { IJoinable joinable = type.GetAssociatedJoinable(Factory); @@ -188,17 +190,13 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs assoc.ValidateJoin(path); AddAssociation(assoc); - int nextDepth = currentDepth + 1; - - if (qc == null) + if (qc != null) { - IOuterJoinLoadable pjl = joinable as IOuterJoinLoadable; - if (pjl != null) - WalkEntityTree(pjl, subalias, path, nextDepth); + _joinQueue.Enqueue(new CollectionJoinQueueEntry(qc, subalias, path, pathAlias)); } - else + else if (joinable is IOuterJoinLoadable jl) { - WalkCollectionTree(qc, subalias, path, pathAlias, nextDepth); + _joinQueue.Enqueue(new EntityJoinQueueEntry(jl, subalias, path)); } } @@ -299,7 +297,8 @@ private void AddAssociation(OuterJoinableAssociation association) /// protected void WalkEntityTree(IOuterJoinLoadable persister, string alias) { - WalkEntityTree(persister, alias, string.Empty, 0); + WalkEntityTree(persister, alias, String.Empty); + ProcessJoins(); } /// @@ -307,18 +306,27 @@ protected void WalkEntityTree(IOuterJoinLoadable persister, string alias) /// protected void WalkCollectionTree(IQueryableCollection persister, string alias) { - WalkCollectionTree(persister, alias, string.Empty, string.Empty, 0); - //TODO: when this is the entry point, we should use an INNER_JOIN for fetching the many-to-many elements! + WalkCollectionTree(persister, alias, String.Empty, String.Empty); + ProcessJoins(); + } + + protected void ProcessJoins() + { + while (_joinQueue.Count > 0) + { + var entry = _joinQueue.Dequeue(); + entry.Walk(this); + } } /// /// For a collection role, return a list of associations to be fetched by outerjoin /// - private void WalkCollectionTree(IQueryableCollection persister, string alias, string path, string pathAlias, int currentDepth) + private void WalkCollectionTree(IQueryableCollection persister, string alias, string path, string pathAlias) { if (persister.IsOneToMany) { - WalkEntityTree((IOuterJoinLoadable)persister.ElementPersister, alias, path, currentDepth); + WalkEntityTree((IOuterJoinLoadable) persister.ElementPersister, alias, path); } else { @@ -335,7 +343,7 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st // if the current depth is 0, the root thing being loaded is the // many-to-many collection itself. Here, it is alright to use // an inner join... - bool useInnerJoin = currentDepth == 0; + bool useInnerJoin = _depth == 0; var joinType = GetJoinType( @@ -346,7 +354,7 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st persister.TableName, lhsColumns, !useInnerJoin, - currentDepth - 1, + _depth - 1, null); AddAssociationToJoinTreeIfNecessary( @@ -355,7 +363,6 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st alias, path, pathAlias, - currentDepth - 1, joinType); } else if (type.IsComponentType) @@ -365,8 +372,7 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st persister.ElementColumnNames, persister, alias, - path, - currentDepth); + path); } } } @@ -378,6 +384,8 @@ internal void AddExplicitEntityJoinAssociation( string path, string pathAlias) { + _depth = 0; + visitedAssociationKeys.Clear(); OuterJoinableAssociation assoc = InitAssociation(new OuterJoinableAssociation( persister.EntityType, @@ -400,7 +408,7 @@ internal OuterJoinableAssociation InitAssociation(OuterJoinableAssociation assoc } private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJoinLoadable persister, - int propertyNumber, string alias, string path, bool nullable, int currentDepth, + int propertyNumber, string alias, string path, bool nullable, ILhsAssociationTypeSqlInfo associationTypeSQLInfo) { string[] aliasedLhsColumns = associationTypeSQLInfo.GetAliasedColumnNames(associationType, 0); @@ -421,7 +429,7 @@ private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJ lhsTable, lhsColumns, nullable, - currentDepth, + _depth, persister.GetCascadeStyle(propertyNumber)); AddAssociationToJoinTreeIfNecessary( @@ -430,7 +438,6 @@ private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJ alias, subpath, subPathAlias, - currentDepth, joinType); } } @@ -439,31 +446,45 @@ private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJ /// For an entity class, add to a list of associations to be fetched /// by outerjoin /// - protected virtual void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth) + protected virtual void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path) { int n = persister.CountSubclassProperties(); + + _joinQueue.Enqueue(NextLevelJoinQueueEntry.Instance); for (int i = 0; i < n; i++) { IType type = persister.GetSubclassPropertyType(i); ILhsAssociationTypeSqlInfo associationTypeSQLInfo = JoinHelper.GetLhsSqlInfo(alias, i, persister, Factory); + if (type.IsAssociationType) { WalkEntityAssociationTree((IAssociationType) type, persister, i, alias, path, - persister.IsSubclassPropertyNullable(i), currentDepth, associationTypeSQLInfo); + persister.IsSubclassPropertyNullable(i), associationTypeSQLInfo); } else if (type.IsComponentType) { WalkComponentTree((IAbstractComponentType) type, 0, alias, SubPath(path, persister.GetSubclassPropertyName(i)), - currentDepth, associationTypeSQLInfo); + associationTypeSQLInfo); } } } + /// + /// For an entity class, add to a list of associations to be fetched + /// by outerjoin + /// + // Since 5.4 + [Obsolete("Use or override the overload without the currentDepth parameter")] + protected virtual void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth) + { + WalkEntityTree(persister, alias, path); + } + /// /// For a component, add to a list of associations to be fetched by outerjoin /// protected void WalkComponentTree(IAbstractComponentType componentType, int begin, string alias, string path, - int currentDepth, ILhsAssociationTypeSqlInfo associationTypeSQLInfo) + ILhsAssociationTypeSqlInfo associationTypeSQLInfo) { IType[] types = componentType.Subtypes; string[] propertyNames = componentType.PropertyNames; @@ -492,7 +513,7 @@ protected void WalkComponentTree(IAbstractComponentType componentType, int begin lhsTable, lhsColumns, propertyNullability == null || propertyNullability[i], - currentDepth, + _depth, componentType.GetCascadeStyle(i)); AddAssociationToJoinTreeIfNecessary( @@ -501,7 +522,6 @@ protected void WalkComponentTree(IAbstractComponentType componentType, int begin alias, subpath, subPathAlias, - currentDepth, joinType); } } @@ -509,17 +529,28 @@ protected void WalkComponentTree(IAbstractComponentType componentType, int begin { string subpath = SubPath(path, propertyNames[i]); - WalkComponentTree((IAbstractComponentType) types[i], begin, alias, subpath, currentDepth, associationTypeSQLInfo); + WalkComponentTree((IAbstractComponentType) types[i], begin, alias, subpath, associationTypeSQLInfo); } begin += types[i].GetColumnSpan(Factory); } } + /// + /// For a component, add to a list of associations to be fetched by outerjoin + /// + // Since 5.4 + [Obsolete("Use or override the overload without the currentDepth parameter")] + protected void WalkComponentTree(IAbstractComponentType componentType, int begin, string alias, string path, + int currentDepth, ILhsAssociationTypeSqlInfo associationTypeSQLInfo) + { + WalkComponentTree(componentType, begin, alias, path, associationTypeSQLInfo); + } + /// /// For a composite element, add to a list of associations to be fetched by outerjoin /// private void WalkCompositeElementTree(IAbstractComponentType compositeType, string[] cols, - IQueryableCollection persister, string alias, string path, int currentDepth) + IQueryableCollection persister, string alias, string path) { IType[] types = compositeType.Subtypes; string[] propertyNames = compositeType.PropertyNames; @@ -551,7 +582,7 @@ private void WalkCompositeElementTree(IAbstractComponentType compositeType, stri persister.TableName, lhsColumns, propertyNullability == null || propertyNullability[i], - currentDepth, + _depth, compositeType.GetCascadeStyle(i)); AddAssociationToJoinTreeIfNecessary( @@ -560,7 +591,6 @@ private void WalkCompositeElementTree(IAbstractComponentType compositeType, stri alias, subpath, subPathAlias, - currentDepth, joinType); } } @@ -572,8 +602,7 @@ private void WalkCompositeElementTree(IAbstractComponentType compositeType, stri lhsColumns, persister, alias, - subpath, - currentDepth); + subpath); } begin += length; } @@ -731,6 +760,11 @@ protected virtual string GenerateRootAlias(string description) /// protected virtual bool IsDuplicateAssociation(string foreignKeyTable, string[] foreignKeyColumns) { + if (!Factory.Settings.DetectFetchLoops) + { + return false; + } + AssociationKey associationKey = new AssociationKey(foreignKeyColumns, foreignKeyTable); return !visitedAssociationKeys.Add(associationKey); } @@ -1202,5 +1236,64 @@ protected static string GetSelectFragment(OuterJoinableAssociation join, string { return join.GetSelectFragment(entitySuffix, collectionSuffix, next); } + + protected interface IJoinQueueEntry + { + void Walk(JoinWalker walker); + } + + protected class EntityJoinQueueEntry : IJoinQueueEntry + { + private readonly IOuterJoinLoadable _persister; + private readonly string _alias; + private readonly string _path; + + public EntityJoinQueueEntry(IOuterJoinLoadable persister, string alias, string path) + { + _persister = persister; + _alias = alias; + _path = path; + } + + public void Walk(JoinWalker walker) + { + walker.WalkEntityTree(_persister, _alias, _path); + } + } + + protected class CollectionJoinQueueEntry : IJoinQueueEntry + { + private readonly IQueryableCollection _persister; + private readonly string _alias; + private readonly string _path; + private readonly string _pathAlias; + + public CollectionJoinQueueEntry(IQueryableCollection persister, string alias, string path, string pathAlias) + { + _persister = persister; + _alias = alias; + _path = path; + _pathAlias = pathAlias; + } + + public void Walk(JoinWalker walker) + { + walker.WalkCollectionTree(_persister, _alias, _path, _pathAlias); + } + } + + protected class NextLevelJoinQueueEntry : IJoinQueueEntry + { + public static readonly NextLevelJoinQueueEntry Instance = new(); + + private NextLevelJoinQueueEntry() + { + } + + public void Walk(JoinWalker walker) + { + walker._depth++; + } + } } } diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd index cd0077fe010..08d922ad963 100644 --- a/src/NHibernate/nhibernate-configuration.xsd +++ b/src/NHibernate/nhibernate-configuration.xsd @@ -91,6 +91,7 @@ +