Skip to content

Commit d81375a

Browse files
authored
Fix named parameter leaking to query cache (nhibernate#3144)
Cache only required parts of NhLinqExpression in QueryExpressionPlan Fixes nhibernate#3030
1 parent 4db3735 commit d81375a

File tree

8 files changed

+154
-43
lines changed

8 files changed

+154
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using System.Linq;
3+
using NHibernate.Cfg.MappingSchema;
4+
using NHibernate.Mapping.ByCode;
5+
using NUnit.Framework;
6+
7+
namespace NHibernate.Test.NHSpecificTest.GH3030
8+
{
9+
[TestFixture]
10+
public class ByCodeFixture : TestCaseMappingByCode
11+
{
12+
protected override HbmMapping GetMappings()
13+
{
14+
var mapper = new ModelMapper();
15+
mapper.Class<Entity>(
16+
rc =>
17+
{
18+
rc.Table("Entity");
19+
rc.Id(x => x.Id, m => m.Generator(Generators.Assigned));
20+
});
21+
22+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
23+
}
24+
25+
protected override void OnTearDown()
26+
{
27+
using (var session = OpenSession())
28+
using (var transaction = session.BeginTransaction())
29+
{
30+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
31+
transaction.Commit();
32+
}
33+
}
34+
35+
[Test]
36+
public void LinqShouldNotLeakEntityParameters()
37+
{
38+
WeakReference sessionReference = null;
39+
WeakReference firstReference = null;
40+
WeakReference secondReference = null;
41+
42+
new System.Action(
43+
() =>
44+
{
45+
using (var session = ((DebugSessionFactory) Sfi).ActualFactory.OpenSession())
46+
{
47+
var first = new Entity { Id = 1 };
48+
var second = new Entity { Id = 2 };
49+
50+
_ = session.Query<Entity>().FirstOrDefault(f => f == first);
51+
_ = session.Query<Entity>().FirstOrDefault(f => f == second);
52+
53+
sessionReference = new WeakReference(session, true);
54+
firstReference = new WeakReference(first, true);
55+
secondReference = new WeakReference(second, true);
56+
}
57+
})();
58+
59+
GC.Collect();
60+
GC.WaitForPendingFinalizers();
61+
62+
Assert.That(sessionReference.Target, Is.Null);
63+
Assert.That(firstReference.Target, Is.Null);
64+
Assert.That(secondReference.Target, Is.Null);
65+
}
66+
67+
public class Entity
68+
{
69+
public virtual int Id { get; set; }
70+
}
71+
}
72+
}

src/NHibernate/Engine/Query/QueryExpressionPlan.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using NHibernate.Hql;
4-
using NHibernate.Linq;
54

65
namespace NHibernate.Engine.Query
76
{

src/NHibernate/Engine/Query/QueryPlanCache.cs

+16-8
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ public IQueryExpressionPlan GetHQLQueryPlan(IQueryExpression queryExpression, bo
6060
{
6161
log.Debug("unable to locate HQL query plan in cache; generating ({0})", queryExpression.Key);
6262
}
63+
6364
plan = new QueryExpressionPlan(queryExpression, shallow, enabledFilters, factory);
6465
// 6.0 TODO: add "CanCachePlan { get; }" to IQueryExpression interface
6566
if (!(queryExpression is ICacheableQueryExpression linqExpression) || linqExpression.CanCachePlan)
66-
planCache.Put(key, plan);
67+
planCache.Put(key, PreparePlanToCache(plan));
6768
else
6869
log.Debug("Query plan not cacheable");
6970
}
@@ -79,23 +80,30 @@ public IQueryExpressionPlan GetHQLQueryPlan(IQueryExpression queryExpression, bo
7980
return plan;
8081
}
8182

83+
private QueryExpressionPlan PreparePlanToCache(QueryExpressionPlan plan)
84+
{
85+
if (plan.QueryExpression is NhLinqExpression planExpression)
86+
{
87+
return plan.Copy(new NhLinqExpressionCache(planExpression));
88+
}
89+
90+
return plan;
91+
}
92+
8293
private static QueryExpressionPlan CopyIfRequired(QueryExpressionPlan plan, IQueryExpression queryExpression)
8394
{
84-
var planExpression = plan.QueryExpression as NhLinqExpression;
85-
var expression = queryExpression as NhLinqExpression;
86-
if (planExpression != null && expression != null)
95+
if (plan.QueryExpression is NhLinqExpressionCache cache && queryExpression is NhLinqExpression expression)
8796
{
8897
//NH-3413
8998
//Here we have to use original expression.
9099
//In most cases NH do not translate expression in second time, but
91100
// for cases when we have list parameters in query, like @p1.Contains(...),
92101
// it does, and then it uses parameters from first try.
93-
//TODO: cache only required parts of QueryExpression
94102

95103
//NH-3436
96104
// We have to return new instance plan with it's own query expression
97-
// because other treads can override queryexpression of current plan during execution of query if we will use cached instance of plan
98-
expression.CopyExpressionTranslation(planExpression);
105+
// because other treads can override query expression of current plan during execution of query if we will use cached instance of plan
106+
expression.CopyExpressionTranslation(cache);
99107
plan = plan.Copy(expression);
100108
}
101109

@@ -118,7 +126,7 @@ public IQueryExpressionPlan GetFilterQueryPlan(IQueryExpression queryExpression,
118126
plan = new FilterQueryPlan(queryExpression, collectionRole, shallow, enabledFilters, factory);
119127
// 6.0 TODO: add "CanCachePlan { get; }" to IQueryExpression interface
120128
if (!(queryExpression is ICacheableQueryExpression linqExpression) || linqExpression.CanCachePlan)
121-
planCache.Put(key, plan);
129+
planCache.Put(key, PreparePlanToCache(plan));
122130
else
123131
log.Debug("Query plan not cacheable");
124132
}

src/NHibernate/Hql/Ast/ANTLR/ASTQueryTranslatorFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ static IQueryTranslator[] CreateQueryTranslators(
3333

3434
var translators = polymorphicParsers
3535
.ToArray(hql => queryExpression is NhLinqExpression linqExpression
36-
? new QueryTranslatorImpl(queryIdentifier, hql, filters, factory, linqExpression.NamedParameters)
36+
? new QueryTranslatorImpl(queryIdentifier, hql, filters, factory, linqExpression.GetNamedParameterTypes())
3737
: new QueryTranslatorImpl(queryIdentifier, hql, filters, factory));
3838

3939
foreach (var translator in translators)

src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs

+7-21
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public partial class HqlSqlWalker
5555
private readonly LiteralProcessor _literalProcessor;
5656

5757
private readonly IDictionary<string, string> _tokenReplacements;
58-
private readonly IDictionary<string, NamedParameter> _namedParameters;
5958
private readonly IDictionary<IParameterSpecification, IType> _guessedParameterTypes = new Dictionary<IParameterSpecification, IType>();
6059

6160
private JoinType _impliedJoinType;
@@ -65,32 +64,20 @@ public partial class HqlSqlWalker
6564
private IASTFactory _nodeFactory;
6665
private readonly List<AssignmentSpecification> assignmentSpecifications = new List<AssignmentSpecification>();
6766
private int numberOfParametersInSetClause;
68-
private Stack<int> clauseStack=new Stack<int>();
67+
private Stack<int> clauseStack = new Stack<int>();
6968

7069
public HqlSqlWalker(
7170
QueryTranslatorImpl qti,
7271
ISessionFactoryImplementor sfi,
7372
ITreeNodeStream input,
7473
IDictionary<string, string> tokenReplacements,
7574
string collectionRole)
76-
: this(qti, sfi, input, tokenReplacements, null, collectionRole)
77-
{
78-
}
79-
80-
internal HqlSqlWalker(
81-
QueryTranslatorImpl qti,
82-
ISessionFactoryImplementor sfi,
83-
ITreeNodeStream input,
84-
IDictionary<string, string> tokenReplacements,
85-
IDictionary<string, NamedParameter> namedParameters,
86-
string collectionRole)
8775
: this(input)
8876
{
8977
_sessionFactoryHelper = new SessionFactoryHelperExtensions(sfi);
9078
_qti = qti;
9179
_literalProcessor = new LiteralProcessor(this);
9280
_tokenReplacements = tokenReplacements;
93-
_namedParameters = namedParameters;
9481
_collectionFilterRole = collectionRole;
9582
}
9683

@@ -313,8 +300,7 @@ void PostProcessInsert(IASTNode insert)
313300

314301
IASTNode idSelectExprNode = null;
315302

316-
var seqGenerator = generator as SequenceGenerator;
317-
if (seqGenerator != null)
303+
if (generator is SequenceGenerator seqGenerator)
318304
{
319305
string seqName = seqGenerator.GeneratorKey();
320306
string nextval = SessionFactoryHelper.Factory.Dialect.GetSelectSequenceNextValString(seqName);
@@ -1083,17 +1069,17 @@ IASTNode GenerateNamedParameter(IASTNode delimiterNode, IASTNode nameNode)
10831069
);
10841070

10851071
parameter.HqlParameterSpecification = paramSpec;
1086-
if (_namedParameters != null && _namedParameters.TryGetValue(name, out var namedParameter))
1072+
if (_qti.TryGetNamedParameterType(name, out var type, out var isGuessedType))
10871073
{
10881074
// Add the parameter type information so that we are able to calculate functions return types
10891075
// when the parameter is used as an argument.
1090-
if (namedParameter.IsGuessedType)
1076+
if (isGuessedType)
10911077
{
1092-
_guessedParameterTypes[paramSpec] = namedParameter.Type;
1093-
parameter.GuessedType = namedParameter.Type;
1078+
_guessedParameterTypes[paramSpec] = type;
1079+
parameter.GuessedType = type;
10941080
}
10951081
else
1096-
parameter.ExpectedType = namedParameter.Type;
1082+
parameter.ExpectedType = type;
10971083
}
10981084

10991085
_parameters.Add(paramSpec);

src/NHibernate/Hql/Ast/ANTLR/QueryTranslatorImpl.cs

+19-8
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public partial class QueryTranslatorImpl : IFilterTranslator
2929
private readonly string _queryIdentifier;
3030
private readonly IASTNode _stageOneAst;
3131
private readonly ISessionFactoryImplementor _factory;
32-
private readonly IDictionary<string, NamedParameter> _namedParameters;
32+
private readonly IDictionary<string, Tuple<IType, bool>> _namedParameters;
3333

3434
private bool _shallowQuery;
3535
private bool _compiled;
@@ -69,7 +69,7 @@ internal QueryTranslatorImpl(
6969
IASTNode parsedQuery,
7070
IDictionary<string, IFilter> enabledFilters,
7171
ISessionFactoryImplementor factory,
72-
IDictionary<string, NamedParameter> namedParameters)
72+
IDictionary<string, Tuple<IType, bool>> namedParameters)
7373
{
7474
_queryIdentifier = queryIdentifier;
7575
_stageOneAst = parsedQuery;
@@ -454,7 +454,7 @@ private static IStatementExecutor BuildAppropriateStatementExecutor(IStatement s
454454

455455
private HqlSqlTranslator Analyze(string collectionRole)
456456
{
457-
var translator = new HqlSqlTranslator(_stageOneAst, this, _factory, _tokenReplacements, _namedParameters, collectionRole);
457+
var translator = new HqlSqlTranslator(_stageOneAst, this, _factory, _tokenReplacements, collectionRole);
458458

459459
translator.Translate();
460460

@@ -468,6 +468,20 @@ private void ErrorIfDML()
468468
throw new QueryExecutionRequestException("Not supported for DML operations", _queryIdentifier);
469469
}
470470
}
471+
472+
public bool TryGetNamedParameterType(string name, out IType type, out bool isGuessedType)
473+
{
474+
if (_namedParameters == null || !_namedParameters.TryGetValue(name, out var p))
475+
{
476+
type = null;
477+
isGuessedType = false;
478+
return false;
479+
}
480+
481+
type = p.Item1;
482+
isGuessedType = p.Item2;
483+
return true;
484+
}
471485
}
472486

473487
public class HqlParseEngine
@@ -568,23 +582,20 @@ internal class HqlSqlTranslator
568582
private readonly QueryTranslatorImpl _qti;
569583
private readonly ISessionFactoryImplementor _sfi;
570584
private readonly IDictionary<string, string> _tokenReplacements;
571-
private readonly IDictionary<string, NamedParameter> _namedParameters;
572585
private readonly string _collectionRole;
573586
private IStatement _resultAst;
574587

575-
public HqlSqlTranslator(
588+
internal HqlSqlTranslator(
576589
IASTNode ast,
577590
QueryTranslatorImpl qti,
578591
ISessionFactoryImplementor sfi,
579592
IDictionary<string, string> tokenReplacements,
580-
IDictionary<string, NamedParameter> namedParameters,
581593
string collectionRole)
582594
{
583595
_inputAst = ast;
584596
_qti = qti;
585597
_sfi = sfi;
586598
_tokenReplacements = tokenReplacements;
587-
_namedParameters = namedParameters;
588599
_collectionRole = collectionRole;
589600
}
590601

@@ -604,7 +615,7 @@ public IStatement Translate()
604615

605616
var nodes = new BufferedTreeNodeStream(_inputAst);
606617

607-
var hqlSqlWalker = new HqlSqlWalker(_qti, _sfi, nodes, _tokenReplacements, _namedParameters, _collectionRole);
618+
var hqlSqlWalker = new HqlSqlWalker(_qti, _sfi, nodes, _tokenReplacements, _collectionRole);
608619
hqlSqlWalker.TreeAdaptor = new HqlSqlWalkerTreeAdaptor(hqlSqlWalker);
609620

610621
try

src/NHibernate/Linq/NhLinqExpression.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,18 @@ public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter
113113
return DuplicateTree(ExpressionToHqlTranslationResults.Statement.AstNode);
114114
}
115115

116-
internal void CopyExpressionTranslation(NhLinqExpression other)
116+
internal void CopyExpressionTranslation(NhLinqExpressionCache cache)
117117
{
118-
ExpressionToHqlTranslationResults = other.ExpressionToHqlTranslationResults;
119-
ParameterDescriptors = other.ParameterDescriptors;
118+
ExpressionToHqlTranslationResults = cache.ExpressionToHqlTranslationResults;
119+
ParameterDescriptors = cache.ParameterDescriptors;
120120
// Type could have been overridden by translation.
121-
Type = other.Type;
121+
Type = cache.Type;
122+
}
123+
124+
internal IDictionary<string, Tuple<IType, bool>> GetNamedParameterTypes()
125+
{
126+
return _constantToParameterMap.Values.Distinct()
127+
.ToDictionary(p => p.Name, p => System.Tuple.Create(p.Type, p.IsGuessedType));
122128
}
123129

124130
private static IASTNode DuplicateTree(IASTNode ast)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using NHibernate.Engine;
4+
using NHibernate.Engine.Query;
5+
using NHibernate.Hql.Ast.ANTLR.Tree;
6+
7+
namespace NHibernate.Linq
8+
{
9+
internal class NhLinqExpressionCache : IQueryExpression
10+
{
11+
internal NhLinqExpressionCache(NhLinqExpression expression)
12+
{
13+
ExpressionToHqlTranslationResults = expression.ExpressionToHqlTranslationResults ?? throw new ArgumentException("NhLinqExpression is not translated");
14+
Key = expression.Key;
15+
Type = expression.Type;
16+
ParameterDescriptors = expression.ParameterDescriptors;
17+
}
18+
19+
public ExpressionToHqlTranslationResults ExpressionToHqlTranslationResults { get; }
20+
public string Key { get; }
21+
public System.Type Type { get; }
22+
public IList<NamedParameterDescriptor> ParameterDescriptors { get; }
23+
24+
public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter)
25+
{
26+
return ExpressionToHqlTranslationResults.Statement.AstNode;
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)