Skip to content

Commit 36f5cb1

Browse files
NH-2319 - AsQueryable Map support, to be squashed.
1 parent dc954a7 commit 36f5cb1

File tree

9 files changed

+723
-37
lines changed

9 files changed

+723
-37
lines changed

doc/reference/modules/query_linq.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ using NHibernate.Linq;]]></programlisting>
5454
This will be executed as a query on that <literal>cat</literal>'s kittens without loading the
5555
entire collection.
5656
</para>
57+
<para>
58+
If the collection is a map, call <literal>AsQueryable</literal> on its <literal>Values</literal>
59+
property.
60+
</para>
61+
<programlisting><![CDATA[IList<Cat> whiteKittens =
62+
cat.Kittens.Values.AsQueryable()
63+
.Where(k => k.Color == "white")
64+
.ToList();]]></programlisting>
5765
<para>&nbsp;</para>
5866

5967
<para>

src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ public async Task LoadCompositeElementsWithWithSimpleHbmAliasInjectionAsync()
5151
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithSimpleHbmAliasInjection"));
5252

5353
Assert.That(country, Is.Not.Null);
54-
Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
55-
Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
54+
Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
55+
Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
5656
await (CleanupWithPersonsAsync());
5757
}
5858

@@ -63,8 +63,8 @@ public async Task LoadCompositeElementsWithWithComplexHbmAliasInjectionAsync()
6363
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithComplexHbmAliasInjection"));
6464

6565
Assert.That(country, Is.Not.Null);
66-
Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
67-
Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
66+
Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
67+
Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
6868
await (CleanupWithPersonsAsync());
6969
}
7070

@@ -75,8 +75,8 @@ public async Task LoadCompositeElementsWithWithCustomAliasesAsync()
7575
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithCustomAliases"));
7676

7777
Assert.That(country, Is.Not.Null);
78-
Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
79-
Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
78+
Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
79+
Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
8080

8181
await (CleanupWithPersonsAsync());
8282
}
@@ -201,8 +201,8 @@ public async Task LoadCompositeElementCollectionWithCustomLoaderAsync()
201201
{
202202
var a = await (session.GetAsync<Area>(country.Code));
203203
Assert.That(a, Is.Not.Null, "area");
204-
Assert.That((ICollection) a.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "area.Keys");
205-
Assert.That((ICollection) a.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "area.Elements");
204+
Assert.That(a.Statistics.Keys, Is.EquivalentTo(stats.Keys), "area.Keys");
205+
Assert.That(a.Statistics.Values, Is.EquivalentTo(stats.Values), "area.Elements");
206206
}
207207
await (CleanupWithPersonsAsync());
208208
}
@@ -428,4 +428,4 @@ private static IDictionary<int, AreaStatistics> CreateStatistics()
428428

429429
#endregion
430430
}
431-
}
431+
}
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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;
12+
using System.Linq;
13+
using System.Reflection;
14+
using NHibernate.Cfg;
15+
using NHibernate.Cfg.MappingSchema;
16+
using NHibernate.Collection;
17+
using NHibernate.Engine.Query;
18+
using NHibernate.Mapping.ByCode;
19+
using NHibernate.Util;
20+
using NUnit.Framework;
21+
using NHibernate.Linq;
22+
23+
namespace NHibernate.Test.NHSpecificTest.NH2319
24+
{
25+
using System.Threading.Tasks;
26+
using System.Threading;
27+
[TestFixture]
28+
public class MapFixtureAsync : TestCaseMappingByCode
29+
{
30+
private Guid _parent1Id;
31+
private Guid _child1Id;
32+
private Guid _parent2Id;
33+
private Guid _child3Id;
34+
35+
[Test]
36+
public Task ShouldBeAbleToFindChildrenByNameAsync()
37+
{
38+
return FindChildrenByNameAsync(_parent1Id, _child1Id);
39+
}
40+
41+
private async Task FindChildrenByNameAsync(Guid parentId, Guid childId, CancellationToken cancellationToken = default(CancellationToken))
42+
{
43+
using (var session = OpenSession())
44+
using (session.BeginTransaction())
45+
{
46+
var parent = await (session.GetAsync<Parent>(parentId, cancellationToken));
47+
48+
Assert.That(parent, Is.Not.Null);
49+
50+
var filtered = await (parent.ChildrenMap.Values
51+
.AsQueryable()
52+
.Where(x => x.Name == "Jack")
53+
.ToListAsync(cancellationToken));
54+
55+
Assert.That(filtered, Has.Count.EqualTo(1));
56+
Assert.That(filtered[0].Id, Is.EqualTo(childId));
57+
}
58+
}
59+
60+
[Test]
61+
public async Task ShouldBeAbleToPerformComplexFilteringAsync()
62+
{
63+
using (var session = OpenSession())
64+
using (session.BeginTransaction())
65+
{
66+
var parent = await (session.GetAsync<Parent>(_parent1Id));
67+
68+
Assert.NotNull(parent);
69+
70+
var filtered = await (parent.ChildrenMap.Values
71+
.AsQueryable()
72+
.Where(x => x.Name == "Piter")
73+
.SelectMany(x => x.GrandChildren)
74+
.Select(x => x.Id)
75+
.CountAsync());
76+
77+
Assert.That(filtered, Is.EqualTo(2));
78+
}
79+
}
80+
81+
[Test]
82+
public async Task ShouldBeAbleToReuseQueryPlanAsync()
83+
{
84+
await (ShouldBeAbleToFindChildrenByNameAsync());
85+
using (var spy = new LogSpy(typeof(QueryPlanCache)))
86+
{
87+
Assert.That(ShouldBeAbleToFindChildrenByNameAsync, Throws.Nothing);
88+
AssertFilterPlanCacheHit(spy);
89+
}
90+
}
91+
92+
[Test]
93+
public async Task ShouldNotMixResultsAsync()
94+
{
95+
await (FindChildrenByNameAsync(_parent1Id, _child1Id));
96+
using (var spy = new LogSpy(typeof(QueryPlanCache)))
97+
{
98+
await (FindChildrenByNameAsync(_parent2Id, _child3Id));
99+
AssertFilterPlanCacheHit(spy);
100+
}
101+
}
102+
103+
[Test]
104+
public async Task ShouldNotInitializeCollectionWhenPerformingQueryAsync()
105+
{
106+
using (var session = OpenSession())
107+
using (session.BeginTransaction())
108+
{
109+
var parent = await (session.GetAsync<Parent>(_parent1Id));
110+
Assert.That(parent, Is.Not.Null);
111+
112+
var persistentCollection = (IPersistentCollection) parent.ChildrenMap;
113+
114+
var filtered = await (parent.ChildrenMap.Values
115+
.AsQueryable()
116+
.Where(x => x.Name == "Jack")
117+
.ToListAsync());
118+
119+
Assert.That(filtered, Has.Count.EqualTo(1));
120+
Assert.That(persistentCollection.WasInitialized, Is.False);
121+
}
122+
}
123+
124+
[Test]
125+
public async Task ShouldPerformSqlQueryEvenIfCollectionAlreadyInitializedAsync()
126+
{
127+
using (var session = OpenSession())
128+
using (session.BeginTransaction())
129+
{
130+
var parent = await (session.GetAsync<Parent>(_parent1Id));
131+
Assert.That(parent, Is.Not.Null);
132+
133+
var loaded = parent.ChildrenMap.ToList();
134+
Assert.That(loaded, Has.Count.EqualTo(2));
135+
136+
var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
137+
138+
var filtered = await (parent.ChildrenMap.Values
139+
.AsQueryable()
140+
.Where(x => x.Name == "Jack")
141+
.ToListAsync());
142+
143+
var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
144+
145+
Assert.That(filtered, Has.Count.EqualTo(1));
146+
Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1));
147+
}
148+
}
149+
150+
[Test]
151+
public async Task TestFilterAsync()
152+
{
153+
using (var session = OpenSession())
154+
using (session.BeginTransaction())
155+
{
156+
var parent = await (session.GetAsync<Parent>(_parent1Id));
157+
Assert.That(parent, Is.Not.Null);
158+
159+
var children = await ((await (session.CreateFilterAsync(parent.ChildrenMap, "where this.Name = 'Jack'")))
160+
.ListAsync<Child>());
161+
162+
Assert.That(children, Has.Count.EqualTo(1));
163+
}
164+
}
165+
166+
[Test]
167+
public async Task TestPlanCacheMissAsync()
168+
{
169+
var internalPlanCache = typeof(QueryPlanCache)
170+
.GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
171+
?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache;
172+
Assert.That(internalPlanCache, Is.Not.Null,
173+
$"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation.");
174+
175+
using (var spy = new LogSpy(typeof(QueryPlanCache)))
176+
{
177+
internalPlanCache.Clear();
178+
await (ShouldBeAbleToFindChildrenByNameAsync());
179+
AssertFilterPlanCacheMiss(spy);
180+
}
181+
}
182+
183+
private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache";
184+
185+
private static void AssertFilterPlanCacheHit(LogSpy spy) =>
186+
// Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed
187+
// rather than only asserting it has been hit.
188+
Assert.That(spy.GetWholeLog(),
189+
Contains.Substring("located collection-filter query plan in cache (")
190+
.And.Not.Contains(_filterPlanCacheMissLog));
191+
192+
private static void AssertFilterPlanCacheMiss(LogSpy spy) =>
193+
Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog));
194+
195+
protected override void Configure(Configuration configuration)
196+
{
197+
configuration.SetProperty("show_sql", "true");
198+
configuration.SetProperty("generate_statistics", "true");
199+
}
200+
201+
protected override void OnSetUp()
202+
{
203+
using (var session = OpenSession())
204+
using (var transaction = session.BeginTransaction())
205+
{
206+
var parent1 = new Parent { Name = "Bob" };
207+
_parent1Id = (Guid) session.Save(parent1);
208+
209+
var parent2 = new Parent { Name = "Martin" };
210+
_parent2Id = (Guid) session.Save(parent2);
211+
212+
var child1 = new Child
213+
{
214+
Name = "Jack",
215+
Parent = parent1
216+
};
217+
_child1Id = (Guid) session.Save(child1);
218+
parent1.ChildrenMap.Add(child1.Id, child1);
219+
220+
var child2 = new Child
221+
{
222+
Name = "Piter",
223+
Parent = parent1
224+
};
225+
session.Save(child2);
226+
parent1.ChildrenMap.Add(child2.Id, child2);
227+
228+
var grandChild1 = new GrandChild
229+
{
230+
Name = "Kate",
231+
Child = child2
232+
};
233+
session.Save(grandChild1);
234+
child2.GrandChildren.Add(grandChild1);
235+
236+
var grandChild2 = new GrandChild
237+
{
238+
Name = "Mary",
239+
Child = child2
240+
};
241+
session.Save(grandChild2);
242+
child2.GrandChildren.Add(grandChild2);
243+
244+
var child3 = new Child
245+
{
246+
Name = "Jack",
247+
Parent = parent2
248+
};
249+
_child3Id = (Guid) session.Save(child3);
250+
parent2.ChildrenMap.Add(child1.Id, child3);
251+
252+
session.Flush();
253+
transaction.Commit();
254+
}
255+
}
256+
257+
protected override void OnTearDown()
258+
{
259+
using (var session = OpenSession())
260+
using (var transaction = session.BeginTransaction())
261+
{
262+
session.Delete("from System.Object");
263+
264+
session.Flush();
265+
transaction.Commit();
266+
}
267+
}
268+
269+
protected override HbmMapping GetMappings()
270+
{
271+
var mapper = new ModelMapper();
272+
mapper.Class<Parent>(rc =>
273+
{
274+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
275+
rc.Property(x => x.Name);
276+
rc.Map(x => x.ChildrenMap,
277+
map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
278+
rel => rel.OneToMany());
279+
});
280+
281+
mapper.Class<Child>(rc =>
282+
{
283+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
284+
rc.Property(x => x.Name);
285+
rc.Set(x => x.GrandChildren,
286+
map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
287+
rel => rel.OneToMany());
288+
});
289+
290+
mapper.Class<GrandChild>(rc =>
291+
{
292+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
293+
rc.Property(x => x.Name);
294+
});
295+
296+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)