Skip to content

Commit 8c7444d

Browse files
bahusoidfredericDelaporte
authored andcommitted
Cache query plan for DML LINQ queries (#2229)
1 parent 8530ca4 commit 8c7444d

File tree

3 files changed

+113
-5
lines changed

3 files changed

+113
-5
lines changed

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
using System.Reflection;
1414
using NHibernate.DomainModel.Northwind.Entities;
1515
using NHibernate.Engine.Query;
16+
using NHibernate.Linq;
1617
using NHibernate.Linq.Visitors;
1718
using NHibernate.Util;
1819
using NUnit.Framework;
19-
using NHibernate.Linq;
2020

2121
namespace NHibernate.Test.Linq
2222
{
@@ -237,6 +237,58 @@ public async Task PlansAreCachedAsync()
237237
}
238238
}
239239

240+
[Test]
241+
public async Task DmlPlansAreCachedAsync()
242+
{
243+
var queryPlanCacheType = typeof(QueryPlanCache);
244+
245+
var cache = (SoftLimitMRUCache)
246+
queryPlanCacheType
247+
.GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic)
248+
.GetValue(Sfi.QueryPlanCache);
249+
cache.Clear();
250+
251+
using (session.BeginTransaction())
252+
{
253+
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {CompanyName = "Constant1"}));
254+
await (db.Customers.Where(c => c.CustomerId == "ALFKI").UpdateAsync(x => new Customer {CompanyName = x.CompanyName}));
255+
Assert.That(
256+
cache,
257+
Has.Count.EqualTo(2),
258+
"Query plans should be cached.");
259+
260+
using (var spy = new LogSpy(queryPlanCacheType))
261+
{
262+
//Queries below should hit plan cache.
263+
using (var sqlSpy = new SqlLogSpy())
264+
{
265+
await (db.Customers.Where(c => c.CustomerId == "ANATR").UpdateAsync(x => new Customer {CompanyName = x.CompanyName}));
266+
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {CompanyName = "Constant2"}));
267+
268+
var sqlEvents = sqlSpy.Appender.GetEvents();
269+
Assert.That(
270+
sqlEvents[0].RenderedMessage,
271+
Does.Contain("ANATR").And.Not.Contain("UNKNOWN").And.Not.Contain("Constant1"),
272+
"Unexpected constant parameter value");
273+
Assert.That(
274+
sqlEvents[1].RenderedMessage,
275+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Not.Contain("Constant1"),
276+
"Unexpected constant parameter value");
277+
}
278+
279+
Assert.That(cache, Has.Count.EqualTo(2), "Additional queries should not cause a plan to be cached.");
280+
Assert.That(
281+
spy.GetWholeLog(),
282+
Does
283+
.Contain("located HQL query plan in cache")
284+
.And.Not.Contain("unable to locate HQL query plan in cache"));
285+
286+
await (db.Customers.Where(c => c.CustomerId == "ANATR").UpdateAsync(x => new Customer {ContactName = x.ContactName}));
287+
Assert.That(cache, Has.Count.EqualTo(3), "Query should be cached");
288+
}
289+
}
290+
}
291+
240292
[Test]
241293
public async Task PlansWithNonParameterizedConstantsAreNotCachedAsync()
242294
{

src/NHibernate.Test/Linq/ConstantTest.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Reflection;
44
using NHibernate.DomainModel.Northwind.Entities;
55
using NHibernate.Engine.Query;
6+
using NHibernate.Linq;
67
using NHibernate.Linq.Visitors;
78
using NHibernate.Util;
89
using NUnit.Framework;
@@ -257,6 +258,58 @@ public void PlansAreCached()
257258
}
258259
}
259260

261+
[Test]
262+
public void DmlPlansAreCached()
263+
{
264+
var queryPlanCacheType = typeof(QueryPlanCache);
265+
266+
var cache = (SoftLimitMRUCache)
267+
queryPlanCacheType
268+
.GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic)
269+
.GetValue(Sfi.QueryPlanCache);
270+
cache.Clear();
271+
272+
using (session.BeginTransaction())
273+
{
274+
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {CompanyName = "Constant1"});
275+
db.Customers.Where(c => c.CustomerId == "ALFKI").Update(x => new Customer {CompanyName = x.CompanyName});
276+
Assert.That(
277+
cache,
278+
Has.Count.EqualTo(2),
279+
"Query plans should be cached.");
280+
281+
using (var spy = new LogSpy(queryPlanCacheType))
282+
{
283+
//Queries below should hit plan cache.
284+
using (var sqlSpy = new SqlLogSpy())
285+
{
286+
db.Customers.Where(c => c.CustomerId == "ANATR").Update(x => new Customer {CompanyName = x.CompanyName});
287+
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {CompanyName = "Constant2"});
288+
289+
var sqlEvents = sqlSpy.Appender.GetEvents();
290+
Assert.That(
291+
sqlEvents[0].RenderedMessage,
292+
Does.Contain("ANATR").And.Not.Contain("UNKNOWN").And.Not.Contain("Constant1"),
293+
"Unexpected constant parameter value");
294+
Assert.That(
295+
sqlEvents[1].RenderedMessage,
296+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Not.Contain("Constant1"),
297+
"Unexpected constant parameter value");
298+
}
299+
300+
Assert.That(cache, Has.Count.EqualTo(2), "Additional queries should not cause a plan to be cached.");
301+
Assert.That(
302+
spy.GetWholeLog(),
303+
Does
304+
.Contain("located HQL query plan in cache")
305+
.And.Not.Contain("unable to locate HQL query plan in cache"));
306+
307+
db.Customers.Where(c => c.CustomerId == "ANATR").Update(x => new Customer {ContactName = x.ContactName});
308+
Assert.That(cache, Has.Count.EqualTo(3), "Query should be cached");
309+
}
310+
}
311+
}
312+
260313
[Test]
261314
public void PlansWithNonParameterizedConstantsAreNotCached()
262315
{

src/NHibernate/Linq/NhLinqExpression.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter
8888

8989
ParameterDescriptors = requiredHqlParameters.AsReadOnly();
9090

91-
CanCachePlan = CanCachePlan &&
92-
// If some constants do not have matching HQL parameters, their values from first query will
93-
// be embedded in the plan and reused for subsequent queries: do not cache the plan.
94-
!ParameterValuesByName
91+
if (QueryMode == QueryMode.Select && CanCachePlan)
92+
{
93+
CanCachePlan =
94+
// If some constants do not have matching HQL parameters, their values from first query will
95+
// be embedded in the plan and reused for subsequent queries: do not cache the plan.
96+
!ParameterValuesByName
9597
.Keys
9698
.Except(requiredHqlParameters.Select(p => p.Name))
9799
.Any();
100+
}
98101

99102
// The ast node may be altered by caller, duplicate it for preserving the original one.
100103
return DuplicateTree(ExpressionToHqlTranslationResults.Statement.AstNode);

0 commit comments

Comments
 (0)