Skip to content

Commit 5b6ca3f

Browse files
maca88fredericDelaporte
authored andcommitted
Add support for caching fetched relations with linq query provider (#1952)
1 parent da429f7 commit 5b6ca3f

21 files changed

+806
-116
lines changed

src/NHibernate.Test/Async/Linq/QueryCacheableTests.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
using System.Linq;
1212
using NHibernate.Cfg;
13+
using NHibernate.DomainModel.Northwind.Entities;
1314
using NHibernate.Linq;
1415
using NUnit.Framework;
1516

@@ -281,5 +282,140 @@ public async Task CanBeCombinedWithFetchAsync()
281282
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count");
282283
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count");
283284
}
285+
286+
[Test]
287+
public async Task FetchIsCachableAsync()
288+
{
289+
Sfi.Statistics.Clear();
290+
await (Sfi.EvictQueriesAsync());
291+
292+
Order order;
293+
294+
using (var s = Sfi.OpenSession())
295+
using (var t = s.BeginTransaction())
296+
{
297+
order = (await (s.Query<Order>()
298+
.WithOptions(o => o.SetCacheable(true))
299+
.Fetch(x => x.Customer)
300+
.FetchMany(x => x.OrderLines)
301+
.ThenFetch(x => x.Product)
302+
.ThenFetchMany(x => x.OrderLines)
303+
.Where(x => x.OrderId == 10248)
304+
.ToListAsync()))
305+
.First();
306+
307+
await (t.CommitAsync());
308+
}
309+
310+
AssertFetchedOrder(order);
311+
312+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count");
313+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count");
314+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count");
315+
316+
Sfi.Statistics.Clear();
317+
318+
using (var s = Sfi.OpenSession())
319+
using (var t = s.BeginTransaction())
320+
{
321+
order = (await (s.Query<Order>()
322+
.WithOptions(o => o.SetCacheable(true))
323+
.Fetch(x => x.Customer)
324+
.FetchMany(x => x.OrderLines)
325+
.ThenFetch(x => x.Product)
326+
.ThenFetchMany(x => x.OrderLines)
327+
.Where(x => x.OrderId == 10248)
328+
.ToListAsync()))
329+
.First();
330+
await (t.CommitAsync());
331+
}
332+
333+
AssertFetchedOrder(order);
334+
335+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count");
336+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count");
337+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count");
338+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count");
339+
340+
}
341+
342+
[Test]
343+
public async Task FutureFetchIsCachableAsync()
344+
{
345+
Sfi.Statistics.Clear();
346+
await (Sfi.EvictQueriesAsync());
347+
var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries;
348+
349+
Order order;
350+
351+
using (var s = Sfi.OpenSession())
352+
using (var t = s.BeginTransaction())
353+
{
354+
s.Query<Order>()
355+
.WithOptions(o => o.SetCacheable(true))
356+
.Fetch(x => x.Customer)
357+
.Where(x => x.OrderId == 10248)
358+
.ToFuture();
359+
360+
order = s.Query<Order>()
361+
.WithOptions(o => o.SetCacheable(true))
362+
.FetchMany(x => x.OrderLines)
363+
.ThenFetch(x => x.Product)
364+
.ThenFetchMany(x => x.OrderLines)
365+
.Where(x => x.OrderId == 10248)
366+
.ToFuture()
367+
.ToList()
368+
.First();
369+
370+
await (t.CommitAsync());
371+
}
372+
373+
AssertFetchedOrder(order);
374+
375+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count");
376+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count");
377+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count");
378+
379+
Sfi.Statistics.Clear();
380+
381+
using (var s = Sfi.OpenSession())
382+
using (var t = s.BeginTransaction())
383+
{
384+
s.Query<Order>()
385+
.WithOptions(o => o.SetCacheable(true))
386+
.Fetch(x => x.Customer)
387+
.Where(x => x.OrderId == 10248)
388+
.ToFuture();
389+
390+
order = s.Query<Order>()
391+
.WithOptions(o => o.SetCacheable(true))
392+
.FetchMany(x => x.OrderLines)
393+
.ThenFetch(x => x.Product)
394+
.ThenFetchMany(x => x.OrderLines)
395+
.Where(x => x.OrderId == 10248)
396+
.ToFuture()
397+
.ToList()
398+
.First();
399+
400+
await (t.CommitAsync());
401+
}
402+
403+
AssertFetchedOrder(order);
404+
405+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count");
406+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count");
407+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count");
408+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count");
409+
}
410+
411+
private static void AssertFetchedOrder(Order order)
412+
{
413+
Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized");
414+
Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized");
415+
Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items");
416+
var orderLine = order.OrderLines.First();
417+
Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized");
418+
Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized");
419+
}
284420
}
285421
}

src/NHibernate.Test/Linq/QueryCacheableTests.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Linq;
22
using NHibernate.Cfg;
3+
using NHibernate.DomainModel.Northwind.Entities;
34
using NHibernate.Linq;
45
using NUnit.Framework;
56

@@ -270,5 +271,140 @@ public void CanBeCombinedWithFetch()
270271
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(5), "Unexpected cache put count");
271272
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count");
272273
}
274+
275+
[Test]
276+
public void FetchIsCachable()
277+
{
278+
Sfi.Statistics.Clear();
279+
Sfi.EvictQueries();
280+
281+
Order order;
282+
283+
using (var s = Sfi.OpenSession())
284+
using (var t = s.BeginTransaction())
285+
{
286+
order = s.Query<Order>()
287+
.WithOptions(o => o.SetCacheable(true))
288+
.Fetch(x => x.Customer)
289+
.FetchMany(x => x.OrderLines)
290+
.ThenFetch(x => x.Product)
291+
.ThenFetchMany(x => x.OrderLines)
292+
.Where(x => x.OrderId == 10248)
293+
.ToList()
294+
.First();
295+
296+
t.Commit();
297+
}
298+
299+
AssertFetchedOrder(order);
300+
301+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count");
302+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count");
303+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(1), "Unexpected cache miss count");
304+
305+
Sfi.Statistics.Clear();
306+
307+
using (var s = Sfi.OpenSession())
308+
using (var t = s.BeginTransaction())
309+
{
310+
order = s.Query<Order>()
311+
.WithOptions(o => o.SetCacheable(true))
312+
.Fetch(x => x.Customer)
313+
.FetchMany(x => x.OrderLines)
314+
.ThenFetch(x => x.Product)
315+
.ThenFetchMany(x => x.OrderLines)
316+
.Where(x => x.OrderId == 10248)
317+
.ToList()
318+
.First();
319+
t.Commit();
320+
}
321+
322+
AssertFetchedOrder(order);
323+
324+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count");
325+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count");
326+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count");
327+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count");
328+
329+
}
330+
331+
[Test]
332+
public void FutureFetchIsCachable()
333+
{
334+
Sfi.Statistics.Clear();
335+
Sfi.EvictQueries();
336+
var multiQueries = Sfi.ConnectionProvider.Driver.SupportsMultipleQueries;
337+
338+
Order order;
339+
340+
using (var s = Sfi.OpenSession())
341+
using (var t = s.BeginTransaction())
342+
{
343+
s.Query<Order>()
344+
.WithOptions(o => o.SetCacheable(true))
345+
.Fetch(x => x.Customer)
346+
.Where(x => x.OrderId == 10248)
347+
.ToFuture();
348+
349+
order = s.Query<Order>()
350+
.WithOptions(o => o.SetCacheable(true))
351+
.FetchMany(x => x.OrderLines)
352+
.ThenFetch(x => x.Product)
353+
.ThenFetchMany(x => x.OrderLines)
354+
.Where(x => x.OrderId == 10248)
355+
.ToFuture()
356+
.ToList()
357+
.First();
358+
359+
t.Commit();
360+
}
361+
362+
AssertFetchedOrder(order);
363+
364+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(multiQueries ? 1 : 2), "Unexpected execution count");
365+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(2), "Unexpected cache put count");
366+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(2), "Unexpected cache miss count");
367+
368+
Sfi.Statistics.Clear();
369+
370+
using (var s = Sfi.OpenSession())
371+
using (var t = s.BeginTransaction())
372+
{
373+
s.Query<Order>()
374+
.WithOptions(o => o.SetCacheable(true))
375+
.Fetch(x => x.Customer)
376+
.Where(x => x.OrderId == 10248)
377+
.ToFuture();
378+
379+
order = s.Query<Order>()
380+
.WithOptions(o => o.SetCacheable(true))
381+
.FetchMany(x => x.OrderLines)
382+
.ThenFetch(x => x.Product)
383+
.ThenFetchMany(x => x.OrderLines)
384+
.Where(x => x.OrderId == 10248)
385+
.ToFuture()
386+
.ToList()
387+
.First();
388+
389+
t.Commit();
390+
}
391+
392+
AssertFetchedOrder(order);
393+
394+
Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(0), "Unexpected execution count");
395+
Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(0), "Unexpected cache put count");
396+
Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count");
397+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(2), "Unexpected cache hit count");
398+
}
399+
400+
private static void AssertFetchedOrder(Order order)
401+
{
402+
Assert.That(NHibernateUtil.IsInitialized(order.Customer), Is.True, "Expected the fetched Customer to be initialized");
403+
Assert.That(NHibernateUtil.IsInitialized(order.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized");
404+
Assert.That(order.OrderLines, Has.Count.EqualTo(3), "Expected the fetched OrderLines to have 3 items");
405+
var orderLine = order.OrderLines.First();
406+
Assert.That(NHibernateUtil.IsInitialized(orderLine.Product), Is.True, "Expected the fetched Product to be initialized");
407+
Assert.That(NHibernateUtil.IsInitialized(orderLine.Product.OrderLines), Is.True, "Expected the fetched OrderLines to be initialized");
408+
}
273409
}
274410
}

src/NHibernate/Async/Cache/StandardQueryCache.cs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Linq;
1515
using NHibernate.Cfg;
1616
using NHibernate.Engine;
17+
using NHibernate.Persister.Collection;
1718
using NHibernate.Type;
1819
using NHibernate.Util;
1920

@@ -296,39 +297,52 @@ private async Task<IList> GetResultFromCacheableAsync(
296297
var returnType = returnTypes[0];
297298

298299
// Skip first element, it is the timestamp
299-
var rows = new List<object>(cacheable.Count - 1);
300300
for (var i = 1; i < cacheable.Count; i++)
301301
{
302-
rows.Add(cacheable[i]);
302+
await (returnType.BeforeAssembleAsync(cacheable[i], session, cancellationToken)).ConfigureAwait(false);
303303
}
304304

305-
foreach (var row in rows)
306-
{
307-
await (returnType.BeforeAssembleAsync(row, session, cancellationToken)).ConfigureAwait(false);
308-
}
309-
310-
foreach (var row in rows)
305+
for (var i = 1; i < cacheable.Count; i++)
311306
{
312-
result.Add(await (returnType.AssembleAsync(row, session, null, cancellationToken)).ConfigureAwait(false));
307+
result.Add(await (returnType.AssembleAsync(cacheable[i], session, null, cancellationToken)).ConfigureAwait(false));
313308
}
314309
}
315310
else
316311
{
312+
var collectionIndexes = new Dictionary<int, ICollectionPersister>();
313+
var nonCollectionTypeIndexes = new List<int>();
314+
for (var i = 0; i < returnTypes.Length; i++)
315+
{
316+
if (returnTypes[i] is CollectionType collectionType)
317+
{
318+
collectionIndexes.Add(i, session.Factory.GetCollectionPersister(collectionType.Role));
319+
}
320+
else
321+
{
322+
nonCollectionTypeIndexes.Add(i);
323+
}
324+
}
325+
317326
// Skip first element, it is the timestamp
318-
var rows = new List<object[]>(cacheable.Count - 1);
319327
for (var i = 1; i < cacheable.Count; i++)
320328
{
321-
rows.Add((object[]) cacheable[i]);
329+
await (TypeHelper.BeforeAssembleAsync((object[]) cacheable[i], returnTypes, session, cancellationToken)).ConfigureAwait(false);
322330
}
323331

324-
foreach (var row in rows)
332+
for (var i = 1; i < cacheable.Count; i++)
325333
{
326-
await (TypeHelper.BeforeAssembleAsync(row, returnTypes, session, cancellationToken)).ConfigureAwait(false);
334+
result.Add(await (TypeHelper.AssembleAsync((object[]) cacheable[i], returnTypes, nonCollectionTypeIndexes, session, cancellationToken)).ConfigureAwait(false));
327335
}
328336

329-
foreach (var row in rows)
337+
// Initialization of the fetched collection must be done at the end in order to be able to batch fetch them
338+
// from the cache or database. The collections were already created in the previous for statement so we only
339+
// have to initialize them.
340+
if (collectionIndexes.Count > 0)
330341
{
331-
result.Add(await (TypeHelper.AssembleAsync(row, returnTypes, session, null, cancellationToken)).ConfigureAwait(false));
342+
for (var i = 1; i < cacheable.Count; i++)
343+
{
344+
await (TypeHelper.InitializeCollectionsAsync((object[]) cacheable[i], (object[]) result[i - 1], collectionIndexes, session, cancellationToken)).ConfigureAwait(false);
345+
}
332346
}
333347
}
334348

src/NHibernate/Async/Impl/MultiCriteriaImpl.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken
185185

186186
object o =
187187
await (loader.GetRowFromResultSetAsync(reader, session, queryParameters, loader.GetLockModes(queryParameters.LockModes),
188-
null, hydratedObjects[i], keys, true,
189-
(persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false);
188+
null, hydratedObjects[i], keys, true, null, null,
189+
(persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false);
190190
if (createSubselects[i])
191191
{
192192
subselectResultKeys[i].Add(keys);

src/NHibernate/Async/Impl/MultiQueryImpl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ protected async Task<List<object>> DoListAsync(CancellationToken cancellationTok
143143

144144
rowCount++;
145145
object result = await (translator.Loader.GetRowFromResultSetAsync(
146-
reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true,
146+
reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, null, null,
147147
(persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false);
148148
tempResults.Add(result);
149149

0 commit comments

Comments
 (0)