Skip to content

Commit a6215f0

Browse files
committed
NH-3488 - Remove code duplication in DML queries
1 parent a21b93c commit a6215f0

14 files changed

+189
-254
lines changed

doc/reference/modules/query_linq.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,9 @@ IList<Cat> oldCats =
544544
In both cases, unspecified properties are not included in the resulting SQL update. This could
545545
be changed for <link linkend="mapping-declaration-version"><literal>version</literal></link> and
546546
<link linkend="mapping-declaration-timestamp"><literal>timestamp</literal></link> properties:
547-
<literal>As</literal> and <literal>Assign</literal> methods take an optional boolean parameter,
548-
<literal>versioned</literal>, which allows incrementing the version. Custom version types
549-
(<literal>NHibernate.Usertype.IUserVersionType</literal>) are not supported.
547+
using <literal>UpdateVersioned</literal> instead of <literal>Update</literal> allows incrementing
548+
the version. Custom version types (<literal>NHibernate.Usertype.IUserVersionType</literal>) are
549+
not supported.
550550
</para>
551551
</sect2>
552552

src/NHibernate.Test/LinqBulkManipulation/Fixture.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ public void IncrementCounterVersion()
542542
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
543543
var count =
544544
s.Query<IntegerVersioned>()
545-
.Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true);
545+
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
546546
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
547547
t.Commit();
548548
}
@@ -571,7 +571,7 @@ public void IncrementTimestampVersion()
571571
{
572572
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
573573
var count = s.Query<TimestampVersioned>()
574-
.Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true);
574+
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
575575
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
576576
t.Commit();
577577
}

src/NHibernate/Linq/Assignment.cs

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/NHibernate/Linq/Assignments.cs

Lines changed: 6 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
43
using System.Linq;
54
using System.Linq.Expressions;
6-
using System.Reflection;
75
using NHibernate.Linq.Visitors;
8-
using NHibernate.Util;
96

107
namespace NHibernate.Linq
118
{
12-
public abstract class Assignments
13-
{
14-
protected static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary<string, object>).GetConstructor(new[] { typeof(int) });
15-
protected static readonly MethodInfo DictionaryAddMethodInfo = ReflectHelper.GetMethod<Dictionary<string, object>>(d => d.Add(null, null));
16-
}
17-
189
/// <summary>
1910
/// Class to hold assignments used in updates and inserts.
2011
/// </summary>
2112
/// <typeparam name="TSource">The type of the entity source of the insert or to update.</typeparam>
2213
/// <typeparam name="TTarget">The type of the entity to insert or to update.</typeparam>
23-
public class Assignments<TSource, TTarget> : Assignments
14+
public class Assignments<TSource, TTarget>
2415
{
25-
private readonly List<Assignment> _sets = new List<Assignment>();
16+
private readonly Dictionary<string, Expression> _assignments = new Dictionary<string, Expression>();
17+
18+
internal IReadOnlyDictionary<string, Expression> List => _assignments;
2619

2720
/// <summary>
2821
/// Sets the specified property.
@@ -36,7 +29,7 @@ public Assignments<TSource, TTarget> Set<TProp>(Expression<Func<TTarget, TProp>>
3629
if (expression == null)
3730
throw new ArgumentNullException(nameof(expression));
3831
var member = GetMemberExpression(property);
39-
_sets.Add(new Assignment(member.GetMemberPath(), expression));
32+
_assignments.Add(member.GetMemberPath(), expression);
4033
return this;
4134
}
4235

@@ -50,7 +43,7 @@ public Assignments<TSource, TTarget> Set<TProp>(Expression<Func<TTarget, TProp>>
5043
public Assignments<TSource, TTarget> Set<TProp>(Expression<Func<TTarget, TProp>> property, TProp value)
5144
{
5245
var member = GetMemberExpression(property);
53-
_sets.Add(new Assignment(member.GetMemberPath(), Expression.Constant(value, typeof(TProp))));
46+
_assignments.Add(member.GetMemberPath(), Expression.Constant(value, typeof(TProp)));
5447
return this;
5548
}
5649

@@ -63,85 +56,5 @@ private static MemberExpression GetMemberExpression<TProp>(Expression<Func<TTarg
6356
throw new ArgumentException($"The property expression must refer to a property of {param.Name}({param.Type.Name})", nameof(property));
6457
return member;
6558
}
66-
67-
/// <summary>
68-
/// Converts the assignments into a lambda expression, which creates a Dictionary&lt;string,object%gt;.
69-
/// </summary>
70-
/// <returns>A lambda expression representing the assignments.</returns>
71-
public LambdaExpression ConvertToDictionaryExpression()
72-
{
73-
var param = Expression.Parameter(typeof(TSource));
74-
var inits = new List<ElementInit>();
75-
foreach (var set in _sets)
76-
{
77-
var setter = set.Expression;
78-
if (setter is LambdaExpression setterLambda)
79-
{
80-
setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param);
81-
}
82-
inits.Add(Expression.ElementInit(DictionaryAddMethodInfo, Expression.Constant(set.PropertyPath),
83-
Expression.Convert(setter, typeof(object))));
84-
}
85-
86-
//The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF.
87-
//The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor,
88-
//which could turn it into a Constant
89-
var listInit = Expression.ListInit(
90-
Expression.New(
91-
DictionaryConstructorInfo,
92-
Expression.Condition(
93-
Expression.Equal(param, Expression.Constant(null, typeof(TSource))),
94-
Expression.Constant(_sets.Count),
95-
Expression.Constant(_sets.Count))),
96-
inits);
97-
98-
return Expression.Lambda(listInit, param);
99-
}
100-
101-
/// <summary>
102-
/// Converts a members initialization expression to assignments. Unset members are ignored and left untouched.
103-
/// </summary>
104-
/// <param name="expression">The expression to convert.</param>
105-
/// <returns>The corresponding assignments.</returns>
106-
public static Assignments<TSource, TTarget> FromExpression(Expression<Func<TSource, TTarget>> expression)
107-
{
108-
if (expression == null)
109-
throw new ArgumentNullException(nameof(expression));
110-
var instance = new Assignments<TSource, TTarget>();
111-
var memberInitExpression = expression.Body as MemberInitExpression ??
112-
throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }");
113-
114-
AddSetsFromBindings(memberInitExpression.Bindings, instance, "", expression.Parameters);
115-
116-
return instance;
117-
}
118-
119-
private static void AddSetsFromBindings(IEnumerable<MemberBinding> bindings, Assignments<TSource, TTarget> instance, string path, ReadOnlyCollection<ParameterExpression> parameters)
120-
{
121-
foreach (var binding in bindings)
122-
{
123-
if (binding.BindingType == MemberBindingType.Assignment) // {Property="Value"}
124-
{
125-
AddSetsFromAssignment((MemberAssignment)binding, instance, path + "." + binding.Member.Name, parameters);
126-
}
127-
else if (binding.BindingType == MemberBindingType.MemberBinding) // {Property={SubProperty="Value}}
128-
{
129-
AddSetsFromBindings(((MemberMemberBinding)binding).Bindings, instance, path + "." + binding.Member.Name, parameters);
130-
}
131-
}
132-
}
133-
134-
private static void AddSetsFromAssignment(MemberAssignment assignment, Assignments<TSource, TTarget> instance, string path, ReadOnlyCollection<ParameterExpression> parameters)
135-
{
136-
// {Property=new Instance{SubProperty="Value"}}
137-
if (assignment.Expression is MemberInitExpression memberInit)
138-
{
139-
AddSetsFromBindings(memberInit.Bindings, instance, path, parameters);
140-
}
141-
else
142-
{
143-
instance._sets.Add(new Assignment(path.Substring(1), Expression.Lambda(assignment.Expression, parameters)));
144-
}
145-
}
14659
}
14760
}

src/NHibernate/Linq/DefaultQueryProvider.cs

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ public interface INhQueryProvider : IQueryProvider
1616
IEnumerable<TResult> ExecuteFuture<TResult>(Expression expression);
1717
IFutureValue<TResult> ExecuteFutureValue<TResult>(Expression expression);
1818
void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLinqExpression nhExpression, IDictionary<string, Tuple<object, IType>> parameters);
19-
int ExecuteDelete(Expression predicate);
20-
int ExecuteUpdate<T>(Expression expression, Assignments<T, T> assignments, bool versioned);
21-
int ExecuteInsert<TInput, TOutput>(Expression expression, Assignments<TInput, TOutput> assignments);
19+
int ExecuteDml<T>(QueryMode queryMode, Expression expression);
2220
}
2321

2422
public class DefaultQueryProvider : INhQueryProvider
@@ -174,31 +172,9 @@ public virtual void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLi
174172
}
175173
}
176174

177-
public int ExecuteDelete(Expression predicate)
175+
public int ExecuteDml<T>(QueryMode queryMode, Expression expression)
178176
{
179-
var nhLinqExpression = new NhLinqDeleteExpression(predicate, Session.Factory);
180-
181-
var query = Session.CreateQuery(nhLinqExpression);
182-
183-
SetParameters(query, nhLinqExpression.ParameterValuesByName);
184-
185-
return query.ExecuteUpdate();
186-
}
187-
188-
public int ExecuteUpdate<T>(Expression expression, Assignments<T, T> assignments, bool versioned)
189-
{
190-
var nhLinqExpression = new NhLinqUpdateExpression<T>(expression, assignments, Session.Factory, versioned);
191-
192-
var query = Session.CreateQuery(nhLinqExpression);
193-
194-
SetParameters(query, nhLinqExpression.ParameterValuesByName);
195-
196-
return query.ExecuteUpdate();
197-
}
198-
199-
public int ExecuteInsert<TSource, TTarget>(Expression expression, Assignments<TSource, TTarget> assignments)
200-
{
201-
var nhLinqExpression = new NhLinqInsertExpression<TSource, TTarget>(expression, assignments, Session.Factory);
177+
var nhLinqExpression = new NhLinqDmlExpression<T>(queryMode, expression, Session.Factory);
202178

203179
var query = Session.CreateQuery(nhLinqExpression);
204180

@@ -207,4 +183,4 @@ public int ExecuteInsert<TSource, TTarget>(Expression expression, Assignments<TS
207183
return query.ExecuteUpdate();
208184
}
209185
}
210-
}
186+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using NHibernate.Linq.Visitors;
7+
using NHibernate.Util;
8+
9+
namespace NHibernate.Linq
10+
{
11+
public class DmlExpressionRewriter
12+
{
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+
17+
readonly IReadOnlyCollection<ParameterExpression> _parameters;
18+
readonly Dictionary<string, Expression> _assignments = new Dictionary<string, Expression>();
19+
20+
DmlExpressionRewriter(IReadOnlyCollection<ParameterExpression> parameters)
21+
{
22+
_parameters = parameters;
23+
}
24+
25+
void AddSettersFromBindings(IEnumerable<MemberBinding> bindings, string path)
26+
{
27+
foreach (var node in bindings)
28+
switch (node.BindingType)
29+
{
30+
case MemberBindingType.Assignment:
31+
AddSettersFromAssignment((MemberAssignment) node, path + "." + node.Member.Name);
32+
break;
33+
case MemberBindingType.MemberBinding:
34+
AddSettersFromBindings(((MemberMemberBinding) node).Bindings, path + "." + node.Member.Name);
35+
break;
36+
default:
37+
throw new InvalidOperationException($"{node.BindingType} is not supported");
38+
}
39+
}
40+
41+
void AddSettersFromAssignment(MemberAssignment assignment, string path)
42+
{
43+
// {Property=new Instance{SubProperty="Value"}}
44+
if (assignment.Expression is MemberInitExpression memberInit)
45+
AddSettersFromBindings(memberInit.Bindings, path);
46+
else
47+
_assignments.Add(path.Substring(1), Expression.Lambda(assignment.Expression, _parameters));
48+
}
49+
50+
/// <summary>
51+
/// Converts the assignments into a lambda expression, which creates a Dictionary&lt;string,object%gt;.
52+
/// </summary>
53+
/// <param name="assignments"></param>
54+
/// <returns>A lambda expression representing the assignments.</returns>
55+
static LambdaExpression ConvertAssignmentsToDictionaryExpression<TSource>(IReadOnlyDictionary<string, Expression> assignments)
56+
{
57+
var param = Expression.Parameter(typeof(TSource));
58+
var inits = new List<ElementInit>();
59+
foreach (var set in assignments)
60+
{
61+
var setter = set.Value;
62+
if (setter is LambdaExpression setterLambda)
63+
setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param);
64+
inits.Add(
65+
Expression.ElementInit(
66+
DictionaryAddMethodInfo,
67+
Expression.Constant(set.Key),
68+
Expression.Convert(setter, typeof(object))));
69+
}
70+
71+
//The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF.
72+
//The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor,
73+
//which could turn it into a Constant
74+
var listInit = Expression.ListInit(
75+
Expression.New(
76+
DictionaryConstructorInfo,
77+
Expression.Condition(
78+
Expression.Equal(param, Expression.Constant(null, typeof(TSource))),
79+
Expression.Constant(assignments.Count),
80+
Expression.Constant(assignments.Count))),
81+
inits);
82+
83+
return Expression.Lambda(listInit, param);
84+
}
85+
86+
public static Expression PrepareExpression<TSource, TTarget>(Expression sourceExpression, Expression<Func<TSource, TTarget>> expression)
87+
{
88+
if (expression == null)
89+
throw new ArgumentNullException(nameof(expression));
90+
91+
var memberInitExpression = expression.Body as MemberInitExpression ??
92+
throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }");
93+
94+
var assignments = ExtractAssignments(expression, memberInitExpression);
95+
return PrepareExpression<TSource>(sourceExpression, assignments);
96+
}
97+
98+
public static Expression PrepareExpression<TSource>(Expression sourceExpression, IReadOnlyDictionary<string, Expression> assignments)
99+
{
100+
var lambda = ConvertAssignmentsToDictionaryExpression<TSource>(assignments);
101+
102+
return Expression.Call(
103+
ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type),
104+
sourceExpression,
105+
Expression.Quote(lambda));
106+
}
107+
108+
static Dictionary<string, Expression> ExtractAssignments<TSource, TTarget>(Expression<Func<TSource, TTarget>> expression, MemberInitExpression memberInitExpression)
109+
{
110+
var instance = new DmlExpressionRewriter(expression.Parameters);
111+
instance.AddSettersFromBindings(memberInitExpression.Bindings, "");
112+
return instance._assignments;
113+
}
114+
}
115+
}

src/NHibernate/Linq/InsertSyntax.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public int Into<TTarget>(Action<Assignments<TSource, TTarget>> assignmentActions
3030
throw new ArgumentNullException(nameof(assignmentActions));
3131
var assignments = new Assignments<TSource, TTarget>();
3232
assignmentActions.Invoke(assignments);
33-
return InsertInto(assignments);
33+
34+
return ExecuteInsert<TTarget>(DmlExpressionRewriter.PrepareExpression<TSource>(_sourceExpression, assignments.List));
3435
}
3536

3637
/// <summary>
@@ -41,13 +42,12 @@ public int Into<TTarget>(Action<Assignments<TSource, TTarget>> assignmentActions
4142
/// <returns>The number of inserted entities.</returns>
4243
public int As<TTarget>(Expression<Func<TSource, TTarget>> expression)
4344
{
44-
var assignments = Assignments<TSource, TTarget>.FromExpression(expression);
45-
return InsertInto(assignments);
45+
return ExecuteInsert<TTarget>(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression));
4646
}
4747

48-
internal int InsertInto<TTarget>(Assignments<TSource, TTarget> assignments)
48+
private int ExecuteInsert<TTarget>(Expression insertExpression)
4949
{
50-
return _provider.ExecuteInsert(_sourceExpression, assignments);
50+
return _provider.ExecuteDml<TTarget>(QueryMode.Insert, insertExpression);
5151
}
5252
}
5353
}

0 commit comments

Comments
 (0)