diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs new file mode 100644 index 00000000000..77229193cbf --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs @@ -0,0 +1,147 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.Transform; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3290 +{ + using System.Threading.Tasks; + [TestFixture(true)] + [TestFixture(false)] + public class FixtureAsync : TestCaseMappingByCode + { + private readonly bool _detectFetchLoops; + + public FixtureAsync(bool detectFetchLoops) + { + _detectFetchLoops = detectFetchLoops; + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb)); + + rc.Property( + x => x.Name + ); + + rc.Set( + x => x.Children, + v => + { + v.Table("EntityToEntity"); + v.Cascade(Mapping.ByCode.Cascade.None); + v.Inverse(true); + v.Key(x => + { + x.Column("ParentId"); + x.NotNullable(true); + }); + v.Lazy(CollectionLazy.Lazy); + v.Fetch(CollectionFetchMode.Join); + }, + h => h.ManyToMany(m => m.Column("ChildId")) + ); + + rc.Set( + x => x.Parents, + v => + { + v.Table("EntityToEntity"); + v.Cascade(Mapping.ByCode.Cascade.All); + + v.Key(x => + { + x.Column("ChildId"); + x.NotNullable(true); + }); + v.Lazy(CollectionLazy.Lazy); + v.Fetch(CollectionFetchMode.Join); + }, + h => h.ManyToMany(m => m.Column("ParentId")) + ); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false"); + configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2"); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + var person = new Entity + { + Name = "pers", + Parents = new HashSet() + }; + session.Save(person); + var job = new Entity + { + Name = "job", + Children = new HashSet() + }; + session.Save(job); + + job.Children.Add(person); + person.Parents.Add(job); + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public async Task QueryWithFetchAsync() + { + using var session = OpenSession(); + using var _ = session.BeginTransaction(); + + var all = await (session + .QueryOver() + .Fetch(SelectMode.Fetch, x => x.Children) + .Fetch(SelectMode.Fetch, x => x.Parents) + .TransformUsing(Transformers.DistinctRootEntity) + .ListAsync()); + + foreach (var entity in all) + { + var isPerson = entity.Name == "pers"; + if (isPerson) + Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique."); + else + Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique."); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs index 0e917134d11..9f85c760a14 100644 --- a/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs +++ b/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace NHibernate.Test.NHSpecificTest.GH3288 { @@ -10,6 +11,7 @@ class TopEntity class MiddleEntity { public virtual int Id { get; set; } + public virtual string Name { get; set; } public virtual ISet Components { get; set; } = new HashSet(); } diff --git a/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml index 94ca5e12a95..b9cd68ded6f 100644 --- a/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml +++ b/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml @@ -13,6 +13,7 @@ + diff --git a/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs new file mode 100644 index 00000000000..0ef176af019 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH3290 +{ + class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual ISet Parents { get; set; } + public virtual ISet Children { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs new file mode 100644 index 00000000000..f7c6fe8dbe0 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.Transform; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3290 +{ + [TestFixture(true)] + [TestFixture(false)] + public class Fixture : TestCaseMappingByCode + { + private readonly bool _detectFetchLoops; + + public Fixture(bool detectFetchLoops) + { + _detectFetchLoops = detectFetchLoops; + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb)); + + rc.Property( + x => x.Name + ); + + rc.Set( + x => x.Children, + v => + { + v.Table("EntityToEntity"); + v.Cascade(Mapping.ByCode.Cascade.None); + v.Inverse(true); + v.Key(x => + { + x.Column("ParentId"); + x.NotNullable(true); + }); + v.Lazy(CollectionLazy.Lazy); + v.Fetch(CollectionFetchMode.Join); + }, + h => h.ManyToMany(m => m.Column("ChildId")) + ); + + rc.Set( + x => x.Parents, + v => + { + v.Table("EntityToEntity"); + v.Cascade(Mapping.ByCode.Cascade.All); + + v.Key(x => + { + x.Column("ChildId"); + x.NotNullable(true); + }); + v.Lazy(CollectionLazy.Lazy); + v.Fetch(CollectionFetchMode.Join); + }, + h => h.ManyToMany(m => m.Column("ParentId")) + ); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false"); + configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2"); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + var person = new Entity + { + Name = "pers", + Parents = new HashSet() + }; + session.Save(person); + var job = new Entity + { + Name = "job", + Children = new HashSet() + }; + session.Save(job); + + job.Children.Add(person); + person.Parents.Add(job); + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public void QueryWithFetch() + { + using var session = OpenSession(); + using var _ = session.BeginTransaction(); + + var all = session + .QueryOver() + .Fetch(SelectMode.Fetch, x => x.Children) + .Fetch(SelectMode.Fetch, x => x.Parents) + .TransformUsing(Transformers.DistinctRootEntity) + .List(); + + foreach (var entity in all) + { + var isPerson = entity.Name == "pers"; + if (isPerson) + Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique."); + else + Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique."); + } + } + } +} diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs index 731ee2cc670..65d7594395c 100644 --- a/src/NHibernate/Loader/JoinWalker.cs +++ b/src/NHibernate/Loader/JoinWalker.cs @@ -192,7 +192,15 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs if (qc != null) { - _joinQueue.Enqueue(new CollectionJoinQueueEntry(qc, subalias, path, pathAlias)); + var collection = new CollectionJoinQueueEntry(qc, subalias, path, pathAlias); + // Many-to-Many element entity join needs to be added right after collection bridge table + // (see IsManyToManyWith, ManyToManySelectFragment, IsManyToManyRoot usages) + if (qc.IsManyToMany) + { + collection.Walk(this); + return; + } + _joinQueue.Enqueue(collection); } else if (joinable is IOuterJoinLoadable jl) {