Skip to content

Commit 86d463a

Browse files
authored
Fix many-to-many fetch issue (#3316)
Fixes #3290
1 parent 03bca68 commit 86d463a

File tree

6 files changed

+309
-2
lines changed

6 files changed

+309
-2
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System.Collections.Generic;
12+
using NHibernate.Cfg;
13+
using NHibernate.Cfg.MappingSchema;
14+
using NHibernate.Mapping.ByCode;
15+
using NHibernate.Transform;
16+
using NUnit.Framework;
17+
18+
namespace NHibernate.Test.NHSpecificTest.GH3290
19+
{
20+
using System.Threading.Tasks;
21+
[TestFixture(true)]
22+
[TestFixture(false)]
23+
public class FixtureAsync : TestCaseMappingByCode
24+
{
25+
private readonly bool _detectFetchLoops;
26+
27+
public FixtureAsync(bool detectFetchLoops)
28+
{
29+
_detectFetchLoops = detectFetchLoops;
30+
}
31+
32+
protected override HbmMapping GetMappings()
33+
{
34+
var mapper = new ModelMapper();
35+
mapper.Class<Entity>(rc =>
36+
{
37+
rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));
38+
39+
rc.Property(
40+
x => x.Name
41+
);
42+
43+
rc.Set(
44+
x => x.Children,
45+
v =>
46+
{
47+
v.Table("EntityToEntity");
48+
v.Cascade(Mapping.ByCode.Cascade.None);
49+
v.Inverse(true);
50+
v.Key(x =>
51+
{
52+
x.Column("ParentId");
53+
x.NotNullable(true);
54+
});
55+
v.Lazy(CollectionLazy.Lazy);
56+
v.Fetch(CollectionFetchMode.Join);
57+
},
58+
h => h.ManyToMany(m => m.Column("ChildId"))
59+
);
60+
61+
rc.Set(
62+
x => x.Parents,
63+
v =>
64+
{
65+
v.Table("EntityToEntity");
66+
v.Cascade(Mapping.ByCode.Cascade.All);
67+
68+
v.Key(x =>
69+
{
70+
x.Column("ChildId");
71+
x.NotNullable(true);
72+
});
73+
v.Lazy(CollectionLazy.Lazy);
74+
v.Fetch(CollectionFetchMode.Join);
75+
},
76+
h => h.ManyToMany(m => m.Column("ParentId"))
77+
);
78+
});
79+
80+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
81+
}
82+
83+
protected override void Configure(Configuration configuration)
84+
{
85+
configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
86+
configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
87+
}
88+
89+
protected override void OnSetUp()
90+
{
91+
using var session = OpenSession();
92+
using var transaction = session.BeginTransaction();
93+
94+
var person = new Entity
95+
{
96+
Name = "pers",
97+
Parents = new HashSet<Entity>()
98+
};
99+
session.Save(person);
100+
var job = new Entity
101+
{
102+
Name = "job",
103+
Children = new HashSet<Entity>()
104+
};
105+
session.Save(job);
106+
107+
job.Children.Add(person);
108+
person.Parents.Add(job);
109+
110+
transaction.Commit();
111+
}
112+
113+
protected override void OnTearDown()
114+
{
115+
using var session = OpenSession();
116+
using var transaction = session.BeginTransaction();
117+
118+
session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
119+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
120+
121+
transaction.Commit();
122+
}
123+
124+
[Test]
125+
public async Task QueryWithFetchAsync()
126+
{
127+
using var session = OpenSession();
128+
using var _ = session.BeginTransaction();
129+
130+
var all = await (session
131+
.QueryOver<Entity>()
132+
.Fetch(SelectMode.Fetch, x => x.Children)
133+
.Fetch(SelectMode.Fetch, x => x.Parents)
134+
.TransformUsing(Transformers.DistinctRootEntity)
135+
.ListAsync());
136+
137+
foreach (var entity in all)
138+
{
139+
var isPerson = entity.Name == "pers";
140+
if (isPerson)
141+
Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
142+
else
143+
Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
144+
}
145+
}
146+
}
147+
}

src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23

34
namespace NHibernate.Test.NHSpecificTest.GH3288
45
{
@@ -10,6 +11,7 @@ class TopEntity
1011
class MiddleEntity
1112
{
1213
public virtual int Id { get; set; }
14+
public virtual string Name { get; set; }
1315
public virtual ISet<Component> Components { get; set; } = new HashSet<Component>();
1416
}
1517

src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<id name="Id" >
1414
<generator class="identity" />
1515
</id>
16+
<property name="Name" />
1617
<set cascade="all" name="Components" table="BottomEntity">
1718
<key not-null="true">
1819
<column name="MiddleEntity_id" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace NHibernate.Test.NHSpecificTest.GH3290
5+
{
6+
class Entity
7+
{
8+
public virtual Guid Id { get; set; }
9+
public virtual string Name { get; set; }
10+
public virtual ISet<Entity> Parents { get; set; }
11+
public virtual ISet<Entity> Children { get; set; }
12+
}
13+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Collections.Generic;
2+
using NHibernate.Cfg;
3+
using NHibernate.Cfg.MappingSchema;
4+
using NHibernate.Mapping.ByCode;
5+
using NHibernate.Transform;
6+
using NUnit.Framework;
7+
8+
namespace NHibernate.Test.NHSpecificTest.GH3290
9+
{
10+
[TestFixture(true)]
11+
[TestFixture(false)]
12+
public class Fixture : TestCaseMappingByCode
13+
{
14+
private readonly bool _detectFetchLoops;
15+
16+
public Fixture(bool detectFetchLoops)
17+
{
18+
_detectFetchLoops = detectFetchLoops;
19+
}
20+
21+
protected override HbmMapping GetMappings()
22+
{
23+
var mapper = new ModelMapper();
24+
mapper.Class<Entity>(rc =>
25+
{
26+
rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));
27+
28+
rc.Property(
29+
x => x.Name
30+
);
31+
32+
rc.Set(
33+
x => x.Children,
34+
v =>
35+
{
36+
v.Table("EntityToEntity");
37+
v.Cascade(Mapping.ByCode.Cascade.None);
38+
v.Inverse(true);
39+
v.Key(x =>
40+
{
41+
x.Column("ParentId");
42+
x.NotNullable(true);
43+
});
44+
v.Lazy(CollectionLazy.Lazy);
45+
v.Fetch(CollectionFetchMode.Join);
46+
},
47+
h => h.ManyToMany(m => m.Column("ChildId"))
48+
);
49+
50+
rc.Set(
51+
x => x.Parents,
52+
v =>
53+
{
54+
v.Table("EntityToEntity");
55+
v.Cascade(Mapping.ByCode.Cascade.All);
56+
57+
v.Key(x =>
58+
{
59+
x.Column("ChildId");
60+
x.NotNullable(true);
61+
});
62+
v.Lazy(CollectionLazy.Lazy);
63+
v.Fetch(CollectionFetchMode.Join);
64+
},
65+
h => h.ManyToMany(m => m.Column("ParentId"))
66+
);
67+
});
68+
69+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
70+
}
71+
72+
protected override void Configure(Configuration configuration)
73+
{
74+
configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
75+
configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
76+
}
77+
78+
protected override void OnSetUp()
79+
{
80+
using var session = OpenSession();
81+
using var transaction = session.BeginTransaction();
82+
83+
var person = new Entity
84+
{
85+
Name = "pers",
86+
Parents = new HashSet<Entity>()
87+
};
88+
session.Save(person);
89+
var job = new Entity
90+
{
91+
Name = "job",
92+
Children = new HashSet<Entity>()
93+
};
94+
session.Save(job);
95+
96+
job.Children.Add(person);
97+
person.Parents.Add(job);
98+
99+
transaction.Commit();
100+
}
101+
102+
protected override void OnTearDown()
103+
{
104+
using var session = OpenSession();
105+
using var transaction = session.BeginTransaction();
106+
107+
session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
108+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
109+
110+
transaction.Commit();
111+
}
112+
113+
[Test]
114+
public void QueryWithFetch()
115+
{
116+
using var session = OpenSession();
117+
using var _ = session.BeginTransaction();
118+
119+
var all = session
120+
.QueryOver<Entity>()
121+
.Fetch(SelectMode.Fetch, x => x.Children)
122+
.Fetch(SelectMode.Fetch, x => x.Parents)
123+
.TransformUsing(Transformers.DistinctRootEntity)
124+
.List();
125+
126+
foreach (var entity in all)
127+
{
128+
var isPerson = entity.Name == "pers";
129+
if (isPerson)
130+
Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
131+
else
132+
Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
133+
}
134+
}
135+
}
136+
}

src/NHibernate/Loader/JoinWalker.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,15 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs
192192

193193
if (qc != null)
194194
{
195-
_joinQueue.Enqueue(new CollectionJoinQueueEntry(qc, subalias, path, pathAlias));
195+
var collection = new CollectionJoinQueueEntry(qc, subalias, path, pathAlias);
196+
// Many-to-Many element entity join needs to be added right after collection bridge table
197+
// (see IsManyToManyWith, ManyToManySelectFragment, IsManyToManyRoot usages)
198+
if (qc.IsManyToMany)
199+
{
200+
collection.Walk(this);
201+
return;
202+
}
203+
_joinQueue.Enqueue(collection);
196204
}
197205
else if (joinable is IOuterJoinLoadable jl)
198206
{

0 commit comments

Comments
 (0)