Skip to content

Commit df6cbbe

Browse files
bahusoidfredericDelaporte
authored andcommitted
Proper query plan caching for DML LINQ queries (#2299)
1 parent 1920603 commit df6cbbe

File tree

5 files changed

+46
-53
lines changed

5 files changed

+46
-53
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,10 @@ public async Task DmlPlansAreCachedAsync()
252252
{
253253
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {CompanyName = "Constant1"}));
254254
await (db.Customers.Where(c => c.CustomerId == "ALFKI").UpdateAsync(x => new Customer {CompanyName = x.CompanyName}));
255+
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {ContactName = "Constant1"}));
255256
Assert.That(
256257
cache,
257-
Has.Count.EqualTo(2),
258+
Has.Count.EqualTo(3),
258259
"Query plans should be cached.");
259260

260261
using (var spy = new LogSpy(queryPlanCacheType))
@@ -264,6 +265,7 @@ public async Task DmlPlansAreCachedAsync()
264265
{
265266
await (db.Customers.Where(c => c.CustomerId == "ANATR").UpdateAsync(x => new Customer {CompanyName = x.CompanyName}));
266267
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {CompanyName = "Constant2"}));
268+
await (db.Customers.Where(c => c.CustomerId == "UNKNOWN").UpdateAsync(x => new Customer {ContactName = "Constant2"}));
267269

268270
var sqlEvents = sqlSpy.Appender.GetEvents();
269271
Assert.That(
@@ -272,19 +274,25 @@ public async Task DmlPlansAreCachedAsync()
272274
"Unexpected constant parameter value");
273275
Assert.That(
274276
sqlEvents[1].RenderedMessage,
275-
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Not.Contain("Constant1"),
277+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Contain("CompanyName").IgnoreCase
278+
.And.Not.Contain("Constant1"),
279+
"Unexpected constant parameter value");
280+
Assert.That(
281+
sqlEvents[2].RenderedMessage,
282+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Contain("ContactName").IgnoreCase
283+
.And.Not.Contain("Constant1"),
276284
"Unexpected constant parameter value");
277285
}
278286

279-
Assert.That(cache, Has.Count.EqualTo(2), "Additional queries should not cause a plan to be cached.");
287+
Assert.That(cache, Has.Count.EqualTo(3), "Additional queries should not cause a plan to be cached.");
280288
Assert.That(
281289
spy.GetWholeLog(),
282290
Does
283291
.Contain("located HQL query plan in cache")
284292
.And.Not.Contain("unable to locate HQL query plan in cache"));
285293

286294
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");
295+
Assert.That(cache, Has.Count.EqualTo(4), "Query should be cached");
288296
}
289297
}
290298
}

src/NHibernate.Test/Linq/ConstantTest.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ public void DmlPlansAreCached()
273273
{
274274
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {CompanyName = "Constant1"});
275275
db.Customers.Where(c => c.CustomerId == "ALFKI").Update(x => new Customer {CompanyName = x.CompanyName});
276+
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {ContactName = "Constant1"});
276277
Assert.That(
277278
cache,
278-
Has.Count.EqualTo(2),
279+
Has.Count.EqualTo(3),
279280
"Query plans should be cached.");
280281

281282
using (var spy = new LogSpy(queryPlanCacheType))
@@ -285,6 +286,7 @@ public void DmlPlansAreCached()
285286
{
286287
db.Customers.Where(c => c.CustomerId == "ANATR").Update(x => new Customer {CompanyName = x.CompanyName});
287288
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {CompanyName = "Constant2"});
289+
db.Customers.Where(c => c.CustomerId == "UNKNOWN").Update(x => new Customer {ContactName = "Constant2"});
288290

289291
var sqlEvents = sqlSpy.Appender.GetEvents();
290292
Assert.That(
@@ -293,19 +295,25 @@ public void DmlPlansAreCached()
293295
"Unexpected constant parameter value");
294296
Assert.That(
295297
sqlEvents[1].RenderedMessage,
296-
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Not.Contain("Constant1"),
298+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Contain("CompanyName").IgnoreCase
299+
.And.Not.Contain("Constant1"),
300+
"Unexpected constant parameter value");
301+
Assert.That(
302+
sqlEvents[2].RenderedMessage,
303+
Does.Contain("UNKNOWN").And.Contain("Constant2").And.Contain("ContactName").IgnoreCase
304+
.And.Not.Contain("Constant1"),
297305
"Unexpected constant parameter value");
298306
}
299307

300-
Assert.That(cache, Has.Count.EqualTo(2), "Additional queries should not cause a plan to be cached.");
308+
Assert.That(cache, Has.Count.EqualTo(3), "Additional queries should not cause a plan to be cached.");
301309
Assert.That(
302310
spy.GetWholeLog(),
303311
Does
304312
.Contain("located HQL query plan in cache")
305313
.And.Not.Contain("unable to locate HQL query plan in cache"));
306314

307315
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");
316+
Assert.That(cache, Has.Count.EqualTo(4), "Query should be cached");
309317
}
310318
}
311319
}

src/NHibernate/Linq/DmlExpressionRewriter.cs

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Linq.Expressions;
5-
using System.Reflection;
65
using NHibernate.Linq.Visitors;
76
using NHibernate.Util;
87

98
namespace NHibernate.Linq
109
{
1110
public class DmlExpressionRewriter
1211
{
13-
static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary<string, object>).GetConstructor(new[] {typeof(int)});
14-
15-
static readonly MethodInfo DictionaryAddMethodInfo = ReflectHelper.GetMethod<Dictionary<string, object>>(d => d.Add(null, null));
16-
1712
readonly IReadOnlyCollection<ParameterExpression> _parameters;
1813
readonly Dictionary<string, Expression> _assignments = new Dictionary<string, Expression>();
1914

@@ -80,39 +75,25 @@ void AddSettersFromAssignment(MemberAssignment assignment, string path)
8075
}
8176

8277
/// <summary>
83-
/// Converts the assignments into a lambda expression, which creates a Dictionary&lt;string,object%gt;.
78+
/// Converts the assignments into block of assignments
8479
/// </summary>
8580
/// <param name="assignments"></param>
8681
/// <returns>A lambda expression representing the assignments.</returns>
87-
static LambdaExpression ConvertAssignmentsToDictionaryExpression<TSource>(IReadOnlyDictionary<string, Expression> assignments)
82+
static LambdaExpression ConvertAssignmentsToBlockExpression<TSource>(IReadOnlyDictionary<string, Expression> assignments)
8883
{
8984
var param = Expression.Parameter(typeof(TSource));
90-
var inits = new List<ElementInit>();
85+
var variableAndAssignmentDic = new Dictionary<ParameterExpression, Expression>(assignments.Count);
9186
foreach (var set in assignments)
9287
{
9388
var setter = set.Value;
9489
if (setter is LambdaExpression setterLambda)
9590
setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param);
96-
inits.Add(
97-
Expression.ElementInit(
98-
DictionaryAddMethodInfo,
99-
Expression.Constant(set.Key),
100-
Expression.Convert(setter, typeof(object))));
91+
92+
var var = Expression.Variable(typeof(object), set.Key);
93+
variableAndAssignmentDic[var] = Expression.Assign(var, Expression.Convert(setter, typeof(object)));
10194
}
10295

103-
//The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF.
104-
//The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor,
105-
//which could turn it into a Constant
106-
var listInit = Expression.ListInit(
107-
Expression.New(
108-
DictionaryConstructorInfo,
109-
Expression.Condition(
110-
Expression.Equal(param, Expression.Constant(null, typeof(TSource))),
111-
Expression.Constant(assignments.Count),
112-
Expression.Constant(assignments.Count))),
113-
inits);
114-
115-
return Expression.Lambda(listInit, param);
96+
return Expression.Lambda(Expression.Block(variableAndAssignmentDic.Keys, variableAndAssignmentDic.Values), param);
11697
}
11798

11899
public static Expression PrepareExpression<TSource, TTarget>(Expression sourceExpression, Expression<Func<TSource, TTarget>> expression)
@@ -151,7 +132,7 @@ public static Expression PrepareExpressionFromAnonymous<TSource>(Expression sour
151132

152133
public static Expression PrepareExpression<TSource>(Expression sourceExpression, IReadOnlyDictionary<string, Expression> assignments)
153134
{
154-
var lambda = ConvertAssignmentsToDictionaryExpression<TSource>(assignments);
135+
var lambda = ConvertAssignmentsToBlockExpression<TSource>(assignments);
155136

156137
return Expression.Call(
157138
ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type),

src/NHibernate/Linq/NhLinqExpression.cs

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

8989
ParameterDescriptors = requiredHqlParameters.AsReadOnly();
9090

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
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
9795
.Keys
9896
.Except(requiredHqlParameters.Select(p => p.Name))
9997
.Any();
100-
}
10198

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

src/NHibernate/Linq/Visitors/QueryModelVisitor.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -438,20 +438,20 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que
438438

439439
private void VisitInsertClause(Expression expression)
440440
{
441-
var listInit = expression as ListInitExpression
441+
var assignments = expression as BlockExpression
442442
?? throw new QueryException("Malformed insert expression");
443443
var insertedType = VisitorParameters.TargetEntityType;
444444
var idents = new List<HqlIdent>();
445445
var selectColumns = new List<HqlExpression>();
446446

447447
//Extract the insert clause from the projected ListInit
448-
foreach (var assignment in listInit.Initializers)
448+
foreach (BinaryExpression assignment in assignments.Expressions)
449449
{
450-
var member = (ConstantExpression)assignment.Arguments[0];
451-
var value = assignment.Arguments[1];
450+
var propName = ((ParameterExpression) assignment.Left).Name;
451+
var value = assignment.Right;
452452

453453
//The target property
454-
idents.Add(_hqlTree.TreeBuilder.Ident((string)member.Value));
454+
idents.Add(_hqlTree.TreeBuilder.Ident(propName));
455455

456456
var valueHql = HqlGeneratorExpressionVisitor.Visit(value, VisitorParameters).AsExpression();
457457
selectColumns.Add(valueHql);
@@ -467,16 +467,15 @@ private void VisitInsertClause(Expression expression)
467467

468468
private void VisitUpdateClause(Expression expression)
469469
{
470-
var listInit = expression as ListInitExpression
470+
var assignments = expression as BlockExpression
471471
?? throw new QueryException("Malformed update expression");
472-
foreach (var initializer in listInit.Initializers)
472+
foreach (BinaryExpression assigment in assignments.Expressions)
473473
{
474-
var member = (ConstantExpression)initializer.Arguments[0];
475-
var setter = initializer.Arguments[1];
474+
var propName = ((ParameterExpression) assigment.Left).Name;
475+
var setter = assigment.Right;
476476
var setterHql = HqlGeneratorExpressionVisitor.Visit(setter, VisitorParameters).AsExpression();
477477

478-
_hqlTree.AddSet(_hqlTree.TreeBuilder.Equality(_hqlTree.TreeBuilder.Ident((string)member.Value),
479-
setterHql));
478+
_hqlTree.AddSet(_hqlTree.TreeBuilder.Equality(_hqlTree.TreeBuilder.Ident(propName), setterHql));
480479
}
481480
}
482481

0 commit comments

Comments
 (0)